import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import { NonNegativeInt, PositiveInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; import { SourceControlProviderError, SourceControlProviderInfo } from "./sourceControl.ts"; import { VcsDriverKind } from "./vcs.ts"; const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; const GIT_LIST_BRANCHES_MAX_LIMIT = 200; // Domain Types export const GitStackedAction = Schema.Literals([ "commit", "push", "create_pr", "commit_push", "commit_push_pr", ]); export type GitStackedAction = typeof GitStackedAction.Type; export const GitActionProgressPhase = Schema.Literals(["branch", "commit", "push", "pr"]); export type GitActionProgressPhase = typeof GitActionProgressPhase.Type; export const GitActionProgressKind = Schema.Literals([ "action_started", "phase_started", "hook_started", "hook_output", "hook_finished", "action_finished", "action_failed", ]); export type GitActionProgressKind = typeof GitActionProgressKind.Type; export const GitActionProgressStream = Schema.Literals(["stdout", "stderr"]); export type GitActionProgressStream = typeof GitActionProgressStream.Type; const GitCommitStepStatus = Schema.Literals([ "created", "skipped_no_changes", "skipped_not_requested", ]); const GitPushStepStatus = Schema.Literals([ "pushed", "skipped_not_requested", "skipped_up_to_date", ]); const GitBranchStepStatus = Schema.Literals(["created", "skipped_not_requested"]); const GitPrStepStatus = Schema.Literals(["created", "opened_existing", "skipped_not_requested"]); const VcsStatusChangeRequestState = Schema.Literals(["open", "closed", "merged"]); const GitPullRequestReference = TrimmedNonEmptyStringSchema; const GitPullRequestState = Schema.Literals(["open", "closed", "merged"]); const GitPreparePullRequestThreadMode = Schema.Literals(["local", "worktree"]); export const GitRunStackedActionToastRunAction = Schema.Struct({ kind: GitStackedAction, }); export type GitRunStackedActionToastRunAction = typeof GitRunStackedActionToastRunAction.Type; const GitRunStackedActionToastCta = Schema.Union([ Schema.Struct({ kind: Schema.Literal("none"), }), Schema.Struct({ kind: Schema.Literal("open_pr"), label: TrimmedNonEmptyStringSchema, url: Schema.String, }), Schema.Struct({ kind: Schema.Literal("run_action"), label: TrimmedNonEmptyStringSchema, action: GitRunStackedActionToastRunAction, }), ]); export type GitRunStackedActionToastCta = typeof GitRunStackedActionToastCta.Type; const GitRunStackedActionToast = Schema.Struct({ title: TrimmedNonEmptyStringSchema, description: Schema.optional(TrimmedNonEmptyStringSchema), cta: GitRunStackedActionToastCta, }); export type GitRunStackedActionToast = typeof GitRunStackedActionToast.Type; export const VcsRef = Schema.Struct({ name: TrimmedNonEmptyStringSchema, isRemote: Schema.optional(Schema.Boolean), remoteName: Schema.optional(TrimmedNonEmptyStringSchema), current: Schema.Boolean, isDefault: Schema.Boolean, worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), }); export type VcsRef = typeof VcsRef.Type; const VcsWorktree = Schema.Struct({ path: TrimmedNonEmptyStringSchema, refName: TrimmedNonEmptyStringSchema, }); const GitResolvedPullRequest = Schema.Struct({ number: PositiveInt, title: TrimmedNonEmptyStringSchema, url: Schema.String, baseBranch: TrimmedNonEmptyStringSchema, headBranch: TrimmedNonEmptyStringSchema, state: GitPullRequestState, }); export type GitResolvedPullRequest = typeof GitResolvedPullRequest.Type; // RPC Inputs export const VcsStatusInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, }); export type VcsStatusInput = typeof VcsStatusInput.Type; export const VcsPullInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, }); export type VcsPullInput = typeof VcsPullInput.Type; export const GitRunStackedActionInput = Schema.Struct({ actionId: TrimmedNonEmptyStringSchema, cwd: TrimmedNonEmptyStringSchema, action: GitStackedAction, commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))), featureBranch: Schema.optional(Schema.Boolean), filePaths: Schema.optional( Schema.Array(TrimmedNonEmptyStringSchema).check(Schema.isMinLength(1)), ), /** The thread the action runs beside; a pull request it creates is linked to it. */ threadId: Schema.optional(ThreadId), }); export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; export const VcsListRefsInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, query: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(256))), cursor: Schema.optional(NonNegativeInt), includeMatchingRemoteRefs: Schema.optional(Schema.Boolean), refKind: Schema.optional(Schema.Literals(["all", "local", "remote"])), refresh: Schema.optional(Schema.Boolean), limit: Schema.optional( PositiveInt.check(Schema.isLessThanOrEqualTo(GIT_LIST_BRANCHES_MAX_LIMIT)), ), }); export type VcsListRefsInput = typeof VcsListRefsInput.Type; export const VcsCreateWorktreeInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, refName: TrimmedNonEmptyStringSchema, newRefName: Schema.optional(TrimmedNonEmptyStringSchema), baseRefName: Schema.optional(TrimmedNonEmptyStringSchema), path: Schema.NullOr(TrimmedNonEmptyStringSchema), }); export type VcsCreateWorktreeInput = typeof VcsCreateWorktreeInput.Type; export const GitPullRequestRefInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, reference: GitPullRequestReference, }); export type GitPullRequestRefInput = typeof GitPullRequestRefInput.Type; export const GitPreparePullRequestThreadInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, reference: GitPullRequestReference, mode: GitPreparePullRequestThreadMode, threadId: Schema.optional(ThreadId), }); export type GitPreparePullRequestThreadInput = typeof GitPreparePullRequestThreadInput.Type; export const VcsRemoveWorktreeInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, path: TrimmedNonEmptyStringSchema, force: Schema.optional(Schema.Boolean), }); export type VcsRemoveWorktreeInput = typeof VcsRemoveWorktreeInput.Type; export const VcsCreateRefInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, refName: TrimmedNonEmptyStringSchema, switchRef: Schema.optional(Schema.Boolean), }); export type VcsCreateRefInput = typeof VcsCreateRefInput.Type; export const VcsCreateRefResult = Schema.Struct({ refName: TrimmedNonEmptyStringSchema, }); export type VcsCreateRefResult = typeof VcsCreateRefResult.Type; export const VcsSwitchRefInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, refName: TrimmedNonEmptyStringSchema, }); export type VcsSwitchRefInput = typeof VcsSwitchRefInput.Type; export const VcsInitInput = Schema.Struct({ cwd: TrimmedNonEmptyStringSchema, kind: Schema.optional(VcsDriverKind), }); export type VcsInitInput = typeof VcsInitInput.Type; // RPC Results const VcsStatusChangeRequest = Schema.Struct({ number: PositiveInt, title: TrimmedNonEmptyStringSchema, url: Schema.String, baseRef: TrimmedNonEmptyStringSchema, headRef: TrimmedNonEmptyStringSchema, state: VcsStatusChangeRequestState, /** Optional for compatibility with older servers and providers. */ isDraft: Schema.optional(Schema.Boolean), /** * Last provider-side activity (ISO), including comments and metadata edits. * This is not the time a change request closed or merged. Optional for old * servers and providers whose lookups do not report it. */ updatedAt: Schema.optional(Schema.NullOr(Schema.String)), }); const VcsStatusLocalShape = { isRepo: Schema.Boolean, sourceControlProvider: Schema.optional(SourceControlProviderInfo), hasPrimaryRemote: Schema.Boolean, isDefaultRef: Schema.Boolean, refName: Schema.NullOr(TrimmedNonEmptyStringSchema), hasWorkingTreeChanges: Schema.Boolean, workingTree: Schema.Struct({ files: Schema.Array( Schema.Struct({ path: TrimmedNonEmptyStringSchema, insertions: NonNegativeInt, deletions: NonNegativeInt, }), ), insertions: NonNegativeInt, deletions: NonNegativeInt, }), }; const VcsStatusRemoteShape = { hasUpstream: Schema.Boolean, aheadCount: NonNegativeInt, behindCount: NonNegativeInt, aheadOfDefaultCount: Schema.optional(NonNegativeInt), pr: Schema.NullOr(VcsStatusChangeRequest), }; export const VcsStatusLocalResult = Schema.Struct(VcsStatusLocalShape); export type VcsStatusLocalResult = typeof VcsStatusLocalResult.Type; export const VcsStatusRemoteResult = Schema.Struct(VcsStatusRemoteShape); export type VcsStatusRemoteResult = typeof VcsStatusRemoteResult.Type; export const VcsStatusResult = Schema.Struct({ ...VcsStatusLocalShape, ...VcsStatusRemoteShape, }); export type VcsStatusResult = typeof VcsStatusResult.Type; export const VcsStatusStreamEvent = Schema.Union([ Schema.TaggedStruct("snapshot", { local: VcsStatusLocalResult, remote: Schema.NullOr(VcsStatusRemoteResult), }), Schema.TaggedStruct("localUpdated", { local: VcsStatusLocalResult, }), Schema.TaggedStruct("remoteUpdated", { remote: Schema.NullOr(VcsStatusRemoteResult), }), ]); export type VcsStatusStreamEvent = typeof VcsStatusStreamEvent.Type; export const VcsListRefsResult = Schema.Struct({ refs: Schema.Array(VcsRef), isRepo: Schema.Boolean, hasPrimaryRemote: Schema.Boolean, nextCursor: NonNegativeInt.pipe(Schema.NullOr), totalCount: NonNegativeInt, }); export type VcsListRefsResult = typeof VcsListRefsResult.Type; export const VcsCreateWorktreeResult = Schema.Struct({ worktree: VcsWorktree, }); export type VcsCreateWorktreeResult = typeof VcsCreateWorktreeResult.Type; export const GitResolvePullRequestResult = Schema.Struct({ pullRequest: GitResolvedPullRequest, }); export type GitResolvePullRequestResult = typeof GitResolvePullRequestResult.Type; export const GitPreparePullRequestThreadResult = Schema.Struct({ pullRequest: GitResolvedPullRequest, branch: TrimmedNonEmptyStringSchema, worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), /** * False when the checkout could not be brought to the pull request head — a reused worktree * holding local commits or uncommitted changes keeps its own state, so the code being handed * over is older than the pull request. */ isOnPullRequestHead: Schema.Boolean.pipe(Schema.withDecodingDefaultKey(Effect.succeed(true))), }); export type GitPreparePullRequestThreadResult = typeof GitPreparePullRequestThreadResult.Type; export const VcsSwitchRefResult = Schema.Struct({ refName: Schema.NullOr(TrimmedNonEmptyStringSchema), }); export type VcsSwitchRefResult = typeof VcsSwitchRefResult.Type; export const GitRunStackedActionResult = Schema.Struct({ action: GitStackedAction, branch: Schema.Struct({ status: GitBranchStepStatus, name: Schema.optional(TrimmedNonEmptyStringSchema), }), commit: Schema.Struct({ status: GitCommitStepStatus, commitSha: Schema.optional(TrimmedNonEmptyStringSchema), subject: Schema.optional(TrimmedNonEmptyStringSchema), }), push: Schema.Struct({ status: GitPushStepStatus, branch: Schema.optional(TrimmedNonEmptyStringSchema), upstreamBranch: Schema.optional(TrimmedNonEmptyStringSchema), setUpstream: Schema.optional(Schema.Boolean), }), pr: Schema.Struct({ status: GitPrStepStatus, url: Schema.optional(Schema.String), number: Schema.optional(PositiveInt), baseBranch: Schema.optional(TrimmedNonEmptyStringSchema), headBranch: Schema.optional(TrimmedNonEmptyStringSchema), title: Schema.optional(TrimmedNonEmptyStringSchema), }), toast: GitRunStackedActionToast, }); export type GitRunStackedActionResult = typeof GitRunStackedActionResult.Type; export const VcsPullResult = Schema.Struct({ status: Schema.Literals(["pulled", "skipped_up_to_date"]), refName: TrimmedNonEmptyStringSchema, upstreamRef: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), }); export type VcsPullResult = typeof VcsPullResult.Type; // RPC / domain errors export class GitCommandError extends Schema.TaggedError()("GitCommandError", { operation: Schema.String, command: Schema.String, cwd: Schema.String, argumentCount: Schema.optional(Schema.Number), exitCode: Schema.optional(Schema.Number), stdoutLength: Schema.optional(Schema.Number), stderrLength: Schema.optional(Schema.Number), outputLength: Schema.optional(Schema.Number), detail: Schema.String, cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return `Git command failed in ${this.operation} (${this.cwd}): ${this.detail}`; } } export class TextGenerationError extends Schema.TaggedError()( "TextGenerationError", { operation: Schema.String, detail: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Text generation failed in ${this.operation}: ${this.detail}`; } } export class GitManagerError extends Schema.TaggedError()("GitManagerError", { operation: Schema.String, cwd: Schema.String, detail: Schema.String, cause: Schema.optional(Schema.Defect()), }) { override get message(): string { return `Git manager failed in ${this.operation}: ${this.detail}`; } } export class GitPullRequestMaterializationError extends Schema.TaggedError()( "GitPullRequestMaterializationError", { cwd: TrimmedNonEmptyStringSchema, pullRequestNumber: PositiveInt, headRepository: Schema.NullOr(TrimmedNonEmptyStringSchema), headBranch: TrimmedNonEmptyStringSchema, localBranch: TrimmedNonEmptyStringSchema, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to materialize pull request #${this.pullRequestNumber} branch ${this.headBranch} as ${this.localBranch}.`; } } export const GitManagerServiceError = Schema.Union([ GitManagerError, GitPullRequestMaterializationError, GitCommandError, SourceControlProviderError, TextGenerationError, ]); export type GitManagerServiceError = typeof GitManagerServiceError.Type; const GitActionProgressBase = Schema.Struct({ actionId: TrimmedNonEmptyStringSchema, cwd: TrimmedNonEmptyStringSchema, action: GitStackedAction, }); const GitActionStartedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("action_started"), phases: Schema.Array(GitActionProgressPhase), }); const GitActionPhaseStartedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("phase_started"), phase: GitActionProgressPhase, label: TrimmedNonEmptyStringSchema, }); const GitActionHookStartedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("hook_started"), hookName: TrimmedNonEmptyStringSchema, }); const GitActionHookOutputEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("hook_output"), hookName: Schema.NullOr(TrimmedNonEmptyStringSchema), stream: GitActionProgressStream, text: TrimmedNonEmptyStringSchema, }); const GitActionHookFinishedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("hook_finished"), hookName: TrimmedNonEmptyStringSchema, exitCode: Schema.NullOr(Schema.Int), durationMs: Schema.NullOr(NonNegativeInt), }); const GitActionFinishedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("action_finished"), result: GitRunStackedActionResult, }); const GitActionFailedEvent = Schema.Struct({ ...GitActionProgressBase.fields, kind: Schema.Literal("action_failed"), phase: Schema.NullOr(GitActionProgressPhase), message: TrimmedNonEmptyStringSchema, }); export const GitActionProgressEvent = Schema.Union([ GitActionStartedEvent, GitActionPhaseStartedEvent, GitActionHookStartedEvent, GitActionHookOutputEvent, GitActionHookFinishedEvent, GitActionFinishedEvent, GitActionFailedEvent, ]); export type GitActionProgressEvent = typeof GitActionProgressEvent.Type;