1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import {
4 ForwardCompatibleOptional,
5 NonNegativeInt,
6 PositiveInt,
7 ProjectId,
8 ThreadId,
9 TrimmedNonEmptyString,
10} from "./baseSchemas.ts";
11import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceControl.ts";
12import { VcsDriverKind } from "./vcs.ts";
13
14const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
15const GitFilePath = Schema.String.check(Schema.isNonEmpty());
16const GIT_LIST_BRANCHES_MAX_LIMIT = 200;
17
18export const GitStackedAction = Schema.Literals([
19 "commit",
20 "push",
21 "create_pr",
22 "commit_push",
23 "commit_push_pr",
24]);
25export type GitStackedAction = typeof GitStackedAction.Type;
26export const GitActionProgressPhase = Schema.Literals(["branch", "commit", "push", "pr"]);
27export type GitActionProgressPhase = typeof GitActionProgressPhase.Type;
28export const GitActionProgressKind = Schema.Literals([
29 "action_started",
30 "phase_started",
31 "hook_started",
32 "hook_output",
33 "hook_finished",
34 "action_finished",
35 "action_failed",
36]);
37export type GitActionProgressKind = typeof GitActionProgressKind.Type;
38export const GitActionProgressStream = Schema.Literals(["stdout", "stderr"]);
39export type GitActionProgressStream = typeof GitActionProgressStream.Type;
40const GitCommitStepStatus = Schema.Literals([
41 "created",
42 "skipped_no_changes",
43 "skipped_not_requested",
44]);
45const GitPushStepStatus = Schema.Literals([
46 "pushed",
47 "skipped_not_requested",
48 "skipped_up_to_date",
49]);
50const GitBranchStepStatus = Schema.Literals(["created", "skipped_not_requested"]);
51const GitPrStepStatus = Schema.Literals(["created", "opened_existing", "skipped_not_requested"]);
52const VcsStatusChangeRequestState = Schema.Literals(["open", "closed", "merged"]);
53const GitPullRequestReference = TrimmedNonEmptyStringSchema;
54const GitPullRequestState = Schema.Literals(["open", "closed", "merged"]);
55const GitPreparePullRequestThreadMode = Schema.Literals(["local", "worktree"]);
56export const GitRunStackedActionToastRunAction = Schema.Struct({
57 kind: GitStackedAction,
58});
59export type GitRunStackedActionToastRunAction = typeof GitRunStackedActionToastRunAction.Type;
60const GitRunStackedActionToastCta = Schema.Union([
61 Schema.Struct({
62 kind: Schema.Literal("none"),
63 }),
64 Schema.Struct({
65 kind: Schema.Literal("open_pr"),
66 label: TrimmedNonEmptyStringSchema,
67 url: Schema.String,
68 }),
69 Schema.Struct({
70 kind: Schema.Literal("run_action"),
71 label: TrimmedNonEmptyStringSchema,
72 action: GitRunStackedActionToastRunAction,
73 }),
74]);
75export type GitRunStackedActionToastCta = typeof GitRunStackedActionToastCta.Type;
76const GitRunStackedActionToast = Schema.Struct({
77 title: TrimmedNonEmptyStringSchema,
78 description: Schema.optional(TrimmedNonEmptyStringSchema),
79 cta: GitRunStackedActionToastCta,
80});
81export type GitRunStackedActionToast = typeof GitRunStackedActionToast.Type;
82
83export const VcsRef = Schema.Struct({
84 name: TrimmedNonEmptyStringSchema,
85 isRemote: Schema.optional(Schema.Boolean),
86 remoteName: Schema.optional(TrimmedNonEmptyStringSchema),
87 // Full Git ref, for example refs/remotes/origin/feature.
88 upstreamRef: Schema.optional(TrimmedNonEmptyStringSchema),
89 current: Schema.Boolean,
90 isDefault: Schema.Boolean,
91 /** Unix seconds of the ref's last commit, for the picker's age column. */
92 lastCommitAt: Schema.optional(NonNegativeInt),
93 worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
94});
95export type VcsRef = typeof VcsRef.Type;
96
97const VcsWorktree = Schema.Struct({
98 path: TrimmedNonEmptyStringSchema,
99 refName: TrimmedNonEmptyStringSchema,
100});
101const GitResolvedPullRequest = Schema.Struct({
102 number: PositiveInt,
103 title: TrimmedNonEmptyStringSchema,
104 url: Schema.String,
105 baseBranch: TrimmedNonEmptyStringSchema,
106 headBranch: TrimmedNonEmptyStringSchema,
107 state: GitPullRequestState,
108});
109export type GitResolvedPullRequest = typeof GitResolvedPullRequest.Type;
110
111export const VcsStatusInput = Schema.Struct({
112 cwd: TrimmedNonEmptyStringSchema,
113});
114export type VcsStatusInput = typeof VcsStatusInput.Type;
115
116export const VcsBranchStatus = Schema.Struct({
117 refName: Schema.NullOr(Schema.String),
118 /** Absent from older servers, where a removed checkout looks like a detached HEAD. */
119 isRepo: Schema.optional(Schema.Boolean),
120});
121export type VcsBranchStatus = typeof VcsBranchStatus.Type;
122
123export const VcsStatusSubscriptionInput = Schema.Struct({
124 ...VcsStatusInput.fields,
125 /** Passive observers receive cached remote status without retaining its refresh loop. */
126 includeRemote: Schema.optional(Schema.Boolean),
127});
128export type VcsStatusSubscriptionInput = typeof VcsStatusSubscriptionInput.Type;
129
130export const VcsPullInput = Schema.Struct({
131 cwd: TrimmedNonEmptyStringSchema,
132});
133export type VcsPullInput = typeof VcsPullInput.Type;
134
135export const GitRunStackedActionInput = Schema.Struct({
136 actionId: TrimmedNonEmptyStringSchema,
137 cwd: TrimmedNonEmptyStringSchema,
138 action: GitStackedAction,
139 commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))),
140 featureBranch: Schema.optional(Schema.Boolean),
141 filePaths: Schema.optional(Schema.Array(GitFilePath).check(Schema.isMinLength(1))),
142 threadId: Schema.optional(ThreadId),
143 projectId: Schema.optional(ProjectId),
144});
145export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type;
146
147export const VcsListRefsInput = Schema.Struct({
148 cwd: TrimmedNonEmptyStringSchema,
149 query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))),
150 cursor: Schema.optional(NonNegativeInt),
151 includeMatchingRemoteRefs: Schema.optional(Schema.Boolean),
152 refKind: Schema.optional(Schema.Literals(["all", "local", "remote"])),
153 refresh: Schema.optional(Schema.Boolean),
154 limit: Schema.optional(
155 PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)),
156 ),
157});
158export type VcsListRefsInput = typeof VcsListRefsInput.Type;
159
160export const VcsCreateWorktreeInput = Schema.Struct({
161 cwd: TrimmedNonEmptyStringSchema,
162 refName: TrimmedNonEmptyStringSchema,
163 newRefName: Schema.optional(TrimmedNonEmptyStringSchema),
164 baseRefName: Schema.optional(TrimmedNonEmptyStringSchema),
165 path: Schema.NullOr(TrimmedNonEmptyStringSchema),
166});
167export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type;
168
169export const GitPullRequestRefInput = Schema.Struct({
170 cwd: TrimmedNonEmptyStringSchema,
171 reference: GitPullRequestReference,
172});
173export type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type;
174
175export const GitPreparePullRequestThreadInput = Schema.Struct({
176 cwd: TrimmedNonEmptyStringSchema,
177 reference: GitPullRequestReference,
178 mode: GitPreparePullRequestThreadMode,
179 threadId: Schema.optional(ThreadId),
180});
181export type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type;
182
183export const VcsRemoveWorktreeInput = Schema.Struct({
184 cwd: TrimmedNonEmptyStringSchema,
185 path: TrimmedNonEmptyStringSchema,
186 force: Schema.optional(Schema.Boolean),
187});
188export type VcsRemoveWorktreeInput = typeof VcsRemoveWorktreeInput.Type;
189
190export const VcsCreateRefInput = Schema.Struct({
191 cwd: TrimmedNonEmptyStringSchema,
192 refName: TrimmedNonEmptyStringSchema,
193 switchRef: Schema.optional(Schema.Boolean),
194});
195export type VcsCreateRefInput = typeof VcsCreateRefInput.Type;
196
197export const VcsCreateRefResult = Schema.Struct({
198 refName: TrimmedNonEmptyStringSchema,
199});
200export type VcsCreateRefResult = typeof VcsCreateRefResult.Type;
201
202export const VcsSwitchRefInput = Schema.Struct({
203 cwd: TrimmedNonEmptyStringSchema,
204 refName: TrimmedNonEmptyStringSchema,
205});
206export type VcsSwitchRefInput = typeof VcsSwitchRefInput.Type;
207
208export const VcsInitInput = Schema.Struct({
209 cwd: TrimmedNonEmptyStringSchema,
210 kind: Schema.optional(VcsDriverKind),
211});
212export type VcsInitInput = typeof VcsInitInput.Type;
213
214const VcsStatusChangeRequest = Schema.Struct({
215 number: PositiveInt,
216 title: TrimmedNonEmptyStringSchema,
217 url: Schema.String,
218 baseRef: TrimmedNonEmptyStringSchema,
219 headRef: TrimmedNonEmptyStringSchema,
220 state: VcsStatusChangeRequestState,
221 isDraft: Schema.optional(Schema.Boolean),
222 updatedAt: Schema.optional(Schema.NullOr(Schema.String)),
223});
224
225/** How a working-tree file differs from HEAD; the files tree labels rows with it. */
226export const VcsWorkingTreeFileStatus = Schema.Literals([
227 "added",
228 "modified",
229 "deleted",
230 "renamed",
231 "untracked",
232]);
233export type VcsWorkingTreeFileStatus = typeof VcsWorkingTreeFileStatus.Type;
234
235const VcsStatusLocalShape = {
236 isRepo: Schema.Boolean,
237 sourceControlProvider: Schema.optional(SourceControlProviderInfo),
238 hasPrimaryRemote: Schema.Boolean,
239 isDefaultRef: Schema.Boolean,
240 refName: Schema.NullOr(TrimmedNonEmptyStringSchema),
241 hasWorkingTreeChanges: Schema.Boolean,
242 workingTree: Schema.Struct({
243 // Absolute worktree root on the environment that every files[].path is
244 // relative to. Absent when the server could not resolve it.
245 rootPath: Schema.optional(GitFilePath),
246 files: Schema.Array(
247 Schema.Struct({
248 path: GitFilePath,
249 insertions: NonNegativeInt,
250 deletions: NonNegativeInt,
251 /** Optional for compatibility with older servers. */
252 status: Schema.optional(VcsWorkingTreeFileStatus),
253 }),
254 ),
255 insertions: NonNegativeInt,
256 deletions: NonNegativeInt,
257 }),
258 branchChanges: Schema.optional(
259 Schema.Struct({
260 baseRef: Schema.NullOr(TrimmedNonEmptyStringSchema),
261 insertions: NonNegativeInt,
262 deletions: NonNegativeInt,
263 }),
264 ),
265};
266
267const VcsStatusRemoteShape = {
268 hasUpstream: Schema.Boolean,
269 aheadCount: NonNegativeInt,
270 behindCount: NonNegativeInt,
271 aheadOfDefaultCount: Schema.optional(NonNegativeInt),
272 pr: Schema.NullOr(VcsStatusChangeRequest),
273};
274
275export const VcsStatusLocalResult = Schema.Struct(VcsStatusLocalShape);
276export type VcsStatusLocalResult = typeof VcsStatusLocalResult.Type;
277
278export const VcsStatusRemoteResult = Schema.Struct(VcsStatusRemoteShape);
279export type VcsStatusRemoteResult = typeof VcsStatusRemoteResult.Type;
280
281export const VcsStatusResult = Schema.Struct({
282 ...VcsStatusLocalShape,
283 ...VcsStatusRemoteShape,
284});
285export type VcsStatusResult = typeof VcsStatusResult.Type;
286
287export const VcsStatusStreamEvent = Schema.Union([
288 Schema.TaggedStruct("snapshot", {
289 local: VcsStatusLocalResult,
290 remote: Schema.NullOr(VcsStatusRemoteResult),
291 }),
292 Schema.TaggedStruct("localUpdated", {
293 local: VcsStatusLocalResult,
294 }),
295 Schema.TaggedStruct("remoteUpdated", {
296 remote: Schema.NullOr(VcsStatusRemoteResult),
297 }),
298]);
299export type VcsStatusStreamEvent = typeof VcsStatusStreamEvent.Type;
300
301export const VcsListRefsResult = Schema.Struct({
302 isAtRepositoryRoot: Schema.optional(Schema.Boolean),
303 refs: Schema.Array(VcsRef),
304 isRepo: Schema.Boolean,
305 hasPrimaryRemote: Schema.Boolean,
306 nextCursor: NonNegativeInt.pipe(Schema.NullOr),
307 totalCount: NonNegativeInt,
308});
309export type VcsListRefsResult = typeof VcsListRefsResult.Type;
310
311export const VcsCreateWorktreeResult = Schema.Struct({
312 worktree: VcsWorktree,
313});
314export type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type;
315
316export const GitResolvePullRequestResult = Schema.Struct({
317 pullRequest: GitResolvedPullRequest,
318});
319export type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type;
320
321export const GitPreparePullRequestThreadResult = Schema.Struct({
322 pullRequest: GitResolvedPullRequest,
323 branch: TrimmedNonEmptyStringSchema,
324 worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
325 /** False when local commits or edits prevented a reused worktree from reaching the PR head. */
326 isOnPullRequestHead: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))),
327});
328export type GitPreparePullRequestThreadResult = typeof GitPreparePullRequestThreadResult.Type;
329
330export const VcsSwitchRefResult = Schema.Struct({
331 refName: Schema.NullOr(TrimmedNonEmptyStringSchema),
332});
333export type VcsSwitchRefResult = typeof VcsSwitchRefResult.Type;
334
335export const GitRunStackedActionResult = Schema.Struct({
336 action: GitStackedAction,
337 branch: Schema.Struct({
338 status: GitBranchStepStatus,
339 name: Schema.optional(TrimmedNonEmptyStringSchema),
340 }),
341 commit: Schema.Struct({
342 status: GitCommitStepStatus,
343 commitSha: Schema.optional(TrimmedNonEmptyStringSchema),
344 subject: Schema.optional(TrimmedNonEmptyStringSchema),
345 }),
346 push: Schema.Struct({
347 status: GitPushStepStatus,
348 branch: Schema.optional(TrimmedNonEmptyStringSchema),
349 upstreamBranch: Schema.optional(TrimmedNonEmptyStringSchema),
350 setUpstream: Schema.optional(Schema.Boolean),
351 }),
352 pr: Schema.Struct({
353 status: GitPrStepStatus,
354 url: Schema.optional(Schema.String),
355 number: Schema.optional(PositiveInt),
356 baseBranch: Schema.optional(TrimmedNonEmptyStringSchema),
357 headBranch: Schema.optional(TrimmedNonEmptyStringSchema),
358 title: Schema.optional(TrimmedNonEmptyStringSchema),
359 }),
360 toast: GitRunStackedActionToast,
361});
362export type GitRunStackedActionResult = typeof GitRunStackedActionResult.Type;
363
364export const VcsPullResult = Schema.Struct({
365 status: Schema.Literals(["pulled", "skipped_up_to_date"]),
366 refName: TrimmedNonEmptyStringSchema,
367 upstreamRef: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr),
368});
369export type VcsPullResult = typeof VcsPullResult.Type;
370
371// Git stderr can echo credentials from argv and remote URLs. Only a fixed
372// reason reaches the error and its diagnostic suffix; newer, unknown reasons
373// decode as absent through GitCommandError.
374export const GitCommandFailureReason = Schema.Literals([
375 "authentication_failed",
376 "branch_already_exists",
377 "branch_checked_out_in_worktree",
378 "config_locked",
379 "host_key_unverified",
380 "not_a_repository",
381 "path_already_exists",
382 "push_rejected_by_remote",
383 "push_rejected_non_fast_forward",
384 "ref_name_conflict",
385 "ref_not_found",
386 "remote_unreachable",
387 "tag_would_be_clobbered",
388]);
389export type GitCommandFailureReason = typeof GitCommandFailureReason.Type;
390
391export class GitCommandError extends Schema.TaggedError<GitCommandError>()("GitCommandError", {
392 operation: Schema.String,
393 command: Schema.String,
394 cwd: Schema.String,
395 argumentCount: Schema.optional(Schema.Number),
396 exitCode: Schema.optional(Schema.Number),
397 stdoutLength: Schema.optional(Schema.Number),
398 stderrLength: Schema.optional(Schema.Number),
399 outputLength: Schema.optional(Schema.Number),
400 reason: ForwardCompatibleOptional(GitCommandFailureReason),
401 detail: Schema.String,
402 cause: Schema.optional(Schema.Defect()),
403}) {
404 override get message(): string {
405 const reason = this.reason === undefined ? "" : ` (${this.reason})`;
406 return `Git command failed in ${this.operation} (${this.cwd}): ${this.detail}${reason}`;
407 }
408}
409
410export class TextGenerationError extends Schema.TaggedError<TextGenerationError>()(
411 "TextGenerationError",
412 {
413 operation: Schema.String,
414 detail: Schema.String,
415 cause: Schema.optional(Schema.Defect()),
416 },
417) {
418 override get message(): string {
419 return `Text generation failed in ${this.operation}: ${this.detail}`;
420 }
421}
422
423export class GitManagerError extends Schema.TaggedError<GitManagerError>()("GitManagerError", {
424 operation: Schema.String,
425 cwd: Schema.String,
426 detail: Schema.String,
427 cause: Schema.optional(Schema.Defect()),
428}) {
429 override get message(): string {
430 return `Git manager failed in ${this.operation}: ${this.detail}`;
431 }
432}
433
434export class GitPullRequestMaterializationError extends Schema.TaggedError<GitPullRequestMaterializationError>()(
435 "GitPullRequestMaterializationError",
436 {
437 cwd: TrimmedNonEmptyStringSchema,
438 pullRequestNumber: PositiveInt,
439 headRepository: Schema.NullOr(TrimmedNonEmptyStringSchema),
440 headBranch: TrimmedNonEmptyStringSchema,
441 localBranch: TrimmedNonEmptyStringSchema,
442 cause: Schema.Defect(),
443 },
444) {
445 override get message(): string {
446 return `Failed to materialize pull request #${this.pullRequestNumber} branch ${this.headBranch} as ${this.localBranch}.`;
447 }
448}
449
450export const GitManagerServiceError = Schema.Union([
451 GitManagerError,
452 GitPullRequestMaterializationError,
453 GitCommandError,
454 SourceControlProviderError,
455 TextGenerationError,
456]);
457export type GitManagerServiceError = typeof GitManagerServiceError.Type;
458
459const GitActionProgressBase = Schema.Struct({
460 actionId: TrimmedNonEmptyStringSchema,
461 cwd: TrimmedNonEmptyStringSchema,
462 action: GitStackedAction,
463});
464
465const GitActionStartedEvent = Schema.Struct({
466 ...GitActionProgressBase.fields,
467 kind: Schema.Literal("action_started"),
468 phases: Schema.Array(GitActionProgressPhase),
469});
470const GitActionPhaseStartedEvent = Schema.Struct({
471 ...GitActionProgressBase.fields,
472 kind: Schema.Literal("phase_started"),
473 phase: GitActionProgressPhase,
474 label: TrimmedNonEmptyStringSchema,
475});
476const GitActionHookStartedEvent = Schema.Struct({
477 ...GitActionProgressBase.fields,
478 kind: Schema.Literal("hook_started"),
479 hookName: TrimmedNonEmptyStringSchema,
480});
481const GitActionHookOutputEvent = Schema.Struct({
482 ...GitActionProgressBase.fields,
483 kind: Schema.Literal("hook_output"),
484 hookName: Schema.NullOr(TrimmedNonEmptyStringSchema),
485 stream: GitActionProgressStream,
486 text: TrimmedNonEmptyStringSchema,
487});
488const GitActionHookFinishedEvent = Schema.Struct({
489 ...GitActionProgressBase.fields,
490 kind: Schema.Literal("hook_finished"),
491 hookName: TrimmedNonEmptyStringSchema,
492 exitCode: Schema.NullOr(Schema.Int),
493 durationMs: Schema.NullOr(NonNegativeInt),
494});
495const GitActionFinishedEvent = Schema.Struct({
496 ...GitActionProgressBase.fields,
497 kind: Schema.Literal("action_finished"),
498 result: GitRunStackedActionResult,
499});
500const GitActionFailedEvent = Schema.Struct({
501 ...GitActionProgressBase.fields,
502 kind: Schema.Literal("action_failed"),
503 phase: Schema.NullOr(GitActionProgressPhase),
504 message: TrimmedNonEmptyStringSchema,
505});
506
507export const GitActionProgressEvent = Schema.Union([
508 GitActionStartedEvent,
509 GitActionPhaseStartedEvent,
510 GitActionHookStartedEvent,
511 GitActionHookOutputEvent,
512 GitActionHookFinishedEvent,
513 GitActionFinishedEvent,
514 GitActionFailedEvent,
515]);
516export type GitActionProgressEvent = typeof GitActionProgressEvent.Type;