import type { GitStatus, GitDiffResponse, GetGitDiffOptions, GetGitRangeDiffOptions, GitFileDiffResponse, GetGitFileDiffOptions, GitBranch, GitDeleteBranchPayload, GitDeleteRemoteBranchPayload, GitRemoveRemotePayload, GeneratedCommitMessage, GitWorktreeInfo, CreateGitWorktreePayload, GitWorktreeCreateResult, RemoveGitWorktreePayload, GitWorktreeValidationResult, CreateGitCommitOptions, GitCommitResult, GitPushResult, GitPullResult, GitPullOptions, GitStashEntry, GitLogOptions, GitLogResponse, GitCommitFilesResponse, CommitFileDiffResponse, GitIdentityProfile, GitIdentitySummary, DiscoveredGitCredential, MergeConflictDetails, CheckoutCommitResponse, CherryPickResponse, RevertCommitResponse, ResetToCommitResponse, } from './api/types'; import { runtimeFetch } from './runtime-fetch'; import { getRuntimeUrlResolver } from './runtime-url'; import { getRuntimeKey } from './runtime-switch'; const API_BASE = '/api/git'; const GIT_STATUS_CACHE_TTL_MS = 1200; const GIT_REPO_CHECK_CACHE_TTL_MS = 5000; const gitStatusCache = new Map(); const gitStatusInFlight = new Map>(); const gitStatusCacheVersions = new Map(); const gitRepoCache = new Map(); const gitRepoInFlight = new Map>(); const normalizeDirectoryKey = (directory: string): string => directory.trim(); const getDirectoryCacheKey = (runtimeKey: string, directory: string): string => JSON.stringify([runtimeKey, normalizeDirectoryKey(directory)]); const getStatusCacheKey = (runtimeKey: string, directory: string, mode?: 'light'): string => JSON.stringify([runtimeKey, normalizeDirectoryKey(directory), mode ?? 'full']); const getStatusCacheVersion = (runtimeKey: string, directory: string): number => gitStatusCacheVersions.get(getDirectoryCacheKey(runtimeKey, directory)) ?? 0; const invalidateGitStatusCache = (directory: string): void => { const runtimeKey = getRuntimeKey(); const key = getDirectoryCacheKey(runtimeKey, directory); gitStatusCacheVersions.set(key, getStatusCacheVersion(runtimeKey, directory) + 1); for (const mode of [undefined, 'light'] as const) { const statusKey = getStatusCacheKey(runtimeKey, directory, mode); gitStatusCache.delete(statusKey); gitStatusInFlight.delete(statusKey); } }; function buildUrl( path: string, directory: string | null | undefined, params?: Record ): string { const query: Record = { ...params }; if (directory) query.directory = directory; return getRuntimeUrlResolver().api(path, query); } export async function checkIsGitRepository(directory: string): Promise { const key = getDirectoryCacheKey(getRuntimeKey(), directory); const now = Date.now(); const cached = gitRepoCache.get(key); if (cached && cached.expiresAt > now) { return cached.value; } const inFlight = gitRepoInFlight.get(key); if (inFlight) { return inFlight; } const task = (async () => { const response = await runtimeFetch(buildUrl(`${API_BASE}/check`, directory)); if (!response.ok) { throw new Error(`Failed to check git repository: ${response.statusText}`); } const data = await response.json(); const isGitRepository = Boolean(data.isGitRepository); gitRepoCache.set(key, { value: isGitRepository, expiresAt: Date.now() + GIT_REPO_CHECK_CACHE_TTL_MS, }); return isGitRepository; })(); gitRepoInFlight.set(key, task); try { return await task; } finally { if (gitRepoInFlight.get(key) === task) { gitRepoInFlight.delete(key); } } } export async function getGitStatus(directory: string, options?: { mode?: 'light' }): Promise { const mode = options?.mode; const runtimeKey = getRuntimeKey(); const key = getStatusCacheKey(runtimeKey, directory, mode); const now = Date.now(); const cached = gitStatusCache.get(key); if (cached && cached.expiresAt > now) { return cached.value; } const inFlight = gitStatusInFlight.get(key); if (inFlight) { return inFlight; } const task = (async () => { const cacheVersion = getStatusCacheVersion(runtimeKey, directory); const response = await runtimeFetch(buildUrl(`${API_BASE}/status`, directory, mode ? { mode } : undefined)); if (!response.ok) { throw new Error(`Failed to get git status: ${response.statusText}`); } const payload = await response.json() as GitStatus; if (getStatusCacheVersion(runtimeKey, directory) === cacheVersion) { gitStatusCache.set(key, { value: payload, expiresAt: Date.now() + GIT_STATUS_CACHE_TTL_MS, }); } return payload; })(); gitStatusInFlight.set(key, task); try { return await task; } finally { if (gitStatusInFlight.get(key) === task) { gitStatusInFlight.delete(key); } } } export async function resolveGitPrimaryRoot(directory: string): Promise<{ root: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/primary-root`, directory)); if (!response.ok) { throw new Error(`Failed to resolve git primary root: ${response.statusText}`); } const payload = await response.json().catch(() => ({})) as { root?: string }; return { root: typeof payload.root === 'string' && payload.root ? payload.root : directory }; } export async function resolveGitTopLevel(directory: string): Promise<{ root: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/toplevel`, directory)); if (!response.ok) { throw new Error(`Failed to resolve git toplevel: ${response.statusText}`); } const payload = await response.json().catch(() => ({})) as { root?: string }; return { root: typeof payload.root === 'string' && payload.root ? payload.root : directory }; } export async function getGitCommitSummaries( directory: string, shas: string[] ): Promise<{ commits: Array<{ sha: string; short: string; subject: string }> }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/commit-summaries`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shas }), }); if (!response.ok) { throw new Error(`Failed to get git commit summaries: ${response.statusText}`); } const payload = await response.json().catch(() => ({})) as { commits?: Array<{ sha?: string; short?: string; subject?: string }>; }; return { commits: Array.isArray(payload.commits) ? payload.commits .map((entry) => ({ sha: typeof entry.sha === 'string' ? entry.sha : '', short: typeof entry.short === 'string' ? entry.short : '', subject: typeof entry.subject === 'string' ? entry.subject : '', })) .filter((entry) => entry.sha && entry.short) : [], }; } export async function getGitDiff(directory: string, options: GetGitDiffOptions): Promise { const { path, staged, contextLines } = options; if (!path) { throw new Error('path is required to fetch git diff'); } const response = await runtimeFetch( buildUrl(`${API_BASE}/diff`, directory, { path, staged: staged ? 'true' : undefined, context: contextLines, }) ); if (!response.ok) { throw new Error(`Failed to get git diff: ${response.statusText}`); } return response.json(); } export async function getGitRangeDiff( directory: string, options: GetGitRangeDiffOptions ): Promise { const { base, head, path, contextLines } = options; if (!base || !head) { throw new Error('base and head are required to fetch git range diff'); } const response = await runtimeFetch( buildUrl(`${API_BASE}/range-diff`, directory, { base, head, path: path || undefined, context: contextLines, }) ); if (!response.ok) { throw new Error(`Failed to get git range diff: ${response.statusText}`); } return response.json(); } export async function getGitFileDiff(directory: string, options: GetGitFileDiffOptions): Promise { const { path, staged } = options; if (!path) { throw new Error('path is required to fetch git file diff'); } const response = await runtimeFetch( buildUrl(`${API_BASE}/file-diff`, directory, { path, staged: staged ? 'true' : undefined, }) ); if (!response.ok) { throw new Error(`Failed to get git file diff: ${response.statusText}`); } return response.json(); } export async function revertGitFile( directory: string, filePath: string, options?: { scope?: 'all' | 'working' } ): Promise { if (!filePath) { throw new Error('path is required to revert git changes'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/revert`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: filePath, scope: options?.scope }), }); if (!response.ok) { const message = await response .json() .catch(() => ({ error: response.statusText })); throw new Error(message.error || 'Failed to revert git changes'); } invalidateGitStatusCache(directory); } export async function stageGitFile(directory: string, filePath: string): Promise { await stageGitFiles(directory, [filePath]); } export async function stageGitFiles(directory: string, filePaths: string[]): Promise { const paths = filePaths.map((path) => path.trim()).filter(Boolean); if (paths.length === 0) { throw new Error('path is required to stage git changes'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/stage`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }), }); if (!response.ok) { const message = await response.json().catch(() => ({ error: response.statusText })); throw new Error(message.error || 'Failed to stage git changes'); } invalidateGitStatusCache(directory); } export async function unstageGitFile(directory: string, filePath: string): Promise { await unstageGitFiles(directory, [filePath]); } export async function unstageGitFiles(directory: string, filePaths: string[]): Promise { const paths = filePaths.map((path) => path.trim()).filter(Boolean); if (paths.length === 0) { throw new Error('path is required to unstage git changes'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/unstage`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths }), }); if (!response.ok) { const message = await response.json().catch(() => ({ error: response.statusText })); throw new Error(message.error || 'Failed to unstage git changes'); } invalidateGitStatusCache(directory); } export async function stageGitHunk(directory: string, filePath: string, patch: string): Promise { await applyGitHunk(directory, filePath, patch, 'stage'); } export async function unstageGitHunk(directory: string, filePath: string, patch: string): Promise { await applyGitHunk(directory, filePath, patch, 'unstage'); } export async function revertGitHunk(directory: string, filePath: string, patch: string): Promise { await applyGitHunk(directory, filePath, patch, 'discard'); } async function applyGitHunk( directory: string, filePath: string, patch: string, action: 'stage' | 'unstage' | 'discard', ): Promise { if (!filePath) { throw new Error('path is required to apply a git hunk'); } if (typeof patch !== 'string' || !patch.trim()) { throw new Error('patch is required to apply a git hunk'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/apply-hunk`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: filePath, patch, action }), }); if (!response.ok) { const message = await response.json().catch(() => ({ error: response.statusText })); throw new Error(message.error || 'Failed to apply git hunk'); } invalidateGitStatusCache(directory); } export async function isLinkedWorktree(directory: string): Promise { if (!directory) { return false; } const response = await runtimeFetch(buildUrl(`${API_BASE}/worktree-type`, directory)); if (!response.ok) { throw new Error(`Failed to detect worktree type: ${response.statusText}`); } const data = await response.json(); return Boolean(data.linked); } export async function getGitBranches(directory: string): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/branches`, directory)); if (!response.ok) { throw new Error(`Failed to get branches: ${response.statusText}`); } return response.json(); } export async function deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }> { if (!payload?.branch) { throw new Error('branch is required to delete a branch'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/branches`, directory), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to delete branch'); } return response.json(); } export async function deleteRemoteBranch(directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> { if (!payload?.branch) { throw new Error('branch is required to delete remote branch'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/remote-branches`, directory), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to delete remote branch'); } return response.json(); } export async function removeRemote(directory: string, payload: GitRemoveRemotePayload): Promise<{ success: boolean }> { const remote = payload?.remote?.trim(); if (!remote) { throw new Error('remote is required to remove a remote'); } const response = await runtimeFetch(buildUrl(`${API_BASE}/remotes`, directory), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ remote }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to remove remote'); } return response.json(); } export async function generateCommitMessage( directory: string, files: string[], options?: { zenModel?: string; providerId?: string; modelId?: string } ): Promise<{ message: GeneratedCommitMessage }> { if (!Array.isArray(files) || files.length === 0) { throw new Error('No files provided to generate commit message'); } const body: Record = { files }; if (options?.zenModel) { body.zenModel = options.zenModel; } if (options?.providerId) { body.providerId = options.providerId; } if (options?.modelId) { body.modelId = options.modelId; } const response = await runtimeFetch(buildUrl(`${API_BASE}/commit-message`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); console.error('[git-generation][browser] http error', { status: response.status, statusText: response.statusText, error, }); const traceSuffix = typeof error?.traceId === 'string' && error.traceId ? ` (traceId: ${error.traceId})` : ''; throw new Error(`${error.error || 'Failed to generate commit message'}${traceSuffix}`); } const data = await response.json(); if (!data?.message || typeof data.message !== 'object') { throw new Error('Malformed commit generation response'); } const subject = typeof data.message.subject === 'string' && data.message.subject.trim().length > 0 ? data.message.subject.trim() : ''; const highlights: string[] = Array.isArray(data.message.highlights) ? (data.message.highlights as unknown[]) .filter((item): item is string => typeof item === 'string' && item.trim().length > 0) .map((item) => (item as string).trim()) : []; return { message: { subject, highlights, }, }; } export async function generatePullRequestDescription( directory: string, payload: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } ): Promise<{ title: string; body: string }> { const { base, head, context, zenModel, providerId, modelId } = payload; if (!base || !head) { throw new Error('base and head are required'); } const requestBody: { base: string; head: string; context?: string; zenModel?: string; providerId?: string; modelId?: string } = { base, head }; if (context?.trim()) { requestBody.context = context.trim(); } if (zenModel) { requestBody.zenModel = zenModel; } if (providerId) { requestBody.providerId = providerId; } if (modelId) { requestBody.modelId = modelId; } const response = await runtimeFetch(buildUrl(`${API_BASE}/pr-description`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to generate PR description'); } const data = await response.json().catch(() => null); const title = typeof data?.title === 'string' ? data.title : ''; const body = typeof data?.body === 'string' ? data.body : ''; if (!title && !body) { throw new Error('Malformed PR description response'); } return { title, body }; } export async function listGitWorktrees(directory: string): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees`, directory)); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to list worktrees'); } return response.json(); } export async function validateGitWorktree(directory: string, payload: CreateGitWorktreePayload): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees/validate`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload ?? {}), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to validate worktree'); } return response.json(); } export async function getGitWorktreeBootstrapStatus(directory: string): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees/bootstrap-status`, directory)); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to get worktree bootstrap status'); } return response.json(); } export async function previewGitWorktree(directory: string, payload: CreateGitWorktreePayload): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees/preview`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload ?? {}), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to preview worktree'); } return response.json(); } export async function createGitWorktree(directory: string, payload: CreateGitWorktreePayload): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload ?? {}), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to create worktree'); } return response.json(); } export async function deleteGitWorktree(directory: string, payload: RemoveGitWorktreePayload): Promise<{ success: boolean }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/worktrees`, directory), { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload ?? {}), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to delete worktree'); } return response.json(); } export async function createGitCommit( directory: string, message: string, options: CreateGitCommitOptions = {} ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/commit`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, addAll: options.addAll ?? false, files: options.files, stageFiles: options.stageFiles, }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to create commit'); } const result = await response.json(); invalidateGitStatusCache(directory); return result; } export async function gitPush( directory: string, options: { remote?: string; branch?: string; options?: string[] | Record } = {} ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/push`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to push'); } const result = await response.json(); invalidateGitStatusCache(directory); return result; } export async function gitPull( directory: string, options: GitPullOptions = {} ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/pull`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to pull'); } const result = await response.json(); invalidateGitStatusCache(directory); return result; } export async function gitFetch( directory: string, options: { remote?: string; branch?: string } = {} ): Promise<{ success: boolean }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/fetch`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to fetch'); } const result = await response.json(); invalidateGitStatusCache(directory); return result; } export async function listGitStashes(directory: string): Promise<{ stashes: GitStashEntry[] }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/stashes`, directory)); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to list stashes'); } return response.json(); } export async function countGitStashFiles(directory: string, refs: string[]): Promise<{ counts: Record }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/stashes/file-counts`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refs }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to count stash files'); } return response.json(); } export async function stashGitChanges(directory: string, options: { message?: string } = {}): Promise<{ success: boolean; created: boolean; message: string; output: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/stash`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to stash changes'); } return response.json(); } const postStashRef = async (directory: string, path: string, options: { ref: string }): Promise<{ success: boolean; ref: string }> => { const response = await runtimeFetch(buildUrl(`${API_BASE}/${path}`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || `Failed to ${path}`); } return response.json(); }; export const applyGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/apply', options); export const popGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/pop', options); export const dropGitStash = (directory: string, options: { ref: string }) => postStashRef(directory, 'stash/drop', options); export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/checkout`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ branch }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to checkout branch'); } return response.json(); } export async function createBranch( directory: string, name: string, startPoint?: string ): Promise<{ success: boolean; branch: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/branches`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, startPoint }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to create branch'); } return response.json(); } export async function renameBranch( directory: string, oldName: string, newName: string ): Promise<{ success: boolean; branch: string }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/branches/rename`, directory), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldName, newName }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to rename branch'); } return response.json(); } export async function getGitLog( directory: string, options: GitLogOptions = {} ): Promise { const response = await runtimeFetch( buildUrl(`${API_BASE}/log`, directory, { maxCount: options.maxCount, from: options.from, to: options.to, file: options.file, all: options.all ? 'true' : undefined, }) ); if (!response.ok) { const errorBody = await response.json().catch(() => ({ error: response.statusText })); throw new Error(`Failed to get git log: ${errorBody.error || response.statusText}`); } return response.json(); } export async function getCommitFiles( directory: string, hash: string ): Promise { const response = await runtimeFetch( buildUrl(`${API_BASE}/commit-files`, directory, { hash }) ); if (!response.ok) { throw new Error(`Failed to get commit files: ${response.statusText}`); } return response.json(); } export async function getCommitFileDiff( directory: string, hash: string, filePath: string, isBinary: boolean ): Promise { const response = await runtimeFetch( buildUrl(`${API_BASE}/commit-file-diff`, directory, { hash, path: filePath, binary: isBinary ? 'true' : undefined, }) ); if (!response.ok) { throw new Error(`Failed to get commit file diff: ${response.statusText}`); } return response.json(); } export async function getGitIdentities(): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/identities`, undefined)); if (!response.ok) { throw new Error(`Failed to get git identities: ${response.statusText}`); } return response.json(); } export async function createGitIdentity(profile: GitIdentityProfile): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/identities`, undefined), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(profile), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to create git identity'); } return response.json(); } export async function updateGitIdentity(id: string, updates: GitIdentityProfile): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/identities/${id}`, undefined), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to update git identity'); } return response.json(); } export async function deleteGitIdentity(id: string): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/identities/${id}`, undefined), { method: 'DELETE', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to delete git identity'); } } export async function getCurrentGitIdentity(directory: string): Promise { if (!directory) { return null; } const response = await runtimeFetch(buildUrl(`${API_BASE}/current-identity`, directory)); if (!response.ok) { throw new Error(`Failed to get current git identity: ${response.statusText}`); } const data = await response.json(); if (!data) { return null; } return { userName: data.userName ?? null, userEmail: data.userEmail ?? null, sshCommand: data.sshCommand ?? null, }; } export async function hasLocalIdentity(directory: string): Promise { if (!directory) { return false; } const response = await runtimeFetch(buildUrl(`${API_BASE}/has-local-identity`, directory)); if (!response.ok) { throw new Error(`Failed to check local identity: ${response.statusText}`); } const data = await response.json().catch(() => null); return data?.hasLocalIdentity === true; } export async function getGlobalGitIdentity(): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/global-identity`, undefined)); if (!response.ok) { throw new Error(`Failed to get global git identity: ${response.statusText}`); } const data = await response.json(); if (!data || (!data.userName && !data.userEmail)) { return null; } return { userName: data.userName ?? null, userEmail: data.userEmail ?? null, sshCommand: data.sshCommand ?? null, }; } export async function setGitIdentity( directory: string, profileId: string ): Promise<{ success: boolean; profile: GitIdentityProfile }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/set-identity`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ profileId }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to set git identity'); } return response.json(); } export async function discoverGitCredentials(): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/discover-credentials`, undefined)); if (!response.ok) { throw new Error(`Failed to discover git credentials: ${response.statusText}`); } return response.json(); } export async function getRemoteUrl(directory: string, remote?: string): Promise { if (!directory) { return null; } const response = await runtimeFetch(buildUrl(`${API_BASE}/remote-url`, directory, { remote })); if (!response.ok) { return null; } const data = await response.json(); return data.url ?? null; } export async function getRemotes(directory: string): Promise> { const response = await runtimeFetch(buildUrl(`${API_BASE}/remotes`, directory)); if (!response.ok) { throw new Error(`Failed to get remotes: ${response.statusText}`); } return response.json(); } export async function rebase( directory: string, options: { onto: string } ): Promise<{ success: boolean; conflict?: boolean; conflictFiles?: string[] }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/rebase`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to rebase'); } return response.json(); } export async function abortRebase(directory: string): Promise<{ success: boolean }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/rebase/abort`, directory), { method: 'POST', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to abort rebase'); } return response.json(); } export async function merge( directory: string, options: { branch: string } ): Promise<{ success: boolean; conflict?: boolean; conflictFiles?: string[] }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/merge`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(options), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to merge'); } return response.json(); } export async function checkoutCommit( directory: string, hash: string ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/checkout-commit`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to checkout commit'); } return response.json(); } export async function cherryPick( directory: string, hash: string ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/cherry-pick`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to cherry-pick'); } return response.json(); } export async function revertCommit( directory: string, hash: string ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/revert-commit`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to revert commit'); } return response.json(); } export async function resetToCommit( directory: string, hash: string, mode: 'soft' | 'mixed' | 'hard', force?: boolean ): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/reset-to-commit`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash, mode, force }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to reset'); } return response.json(); } export async function abortMerge(directory: string): Promise<{ success: boolean }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/merge/abort`, directory), { method: 'POST', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to abort merge'); } return response.json(); } export async function continueRebase(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/rebase/continue`, directory), { method: 'POST', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to continue rebase'); } return response.json(); } export async function continueMerge(directory: string): Promise<{ success: boolean; conflict: boolean; conflictFiles?: string[] }> { const response = await runtimeFetch(buildUrl(`${API_BASE}/merge/continue`, directory), { method: 'POST', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to continue merge'); } return response.json(); } export async function stash( directory: string, options?: { message?: string; includeUntracked?: boolean } ): Promise<{ success: boolean }> { await stashGitChanges(directory, { message: options?.message }); return { success: true }; } export async function stashPop(directory: string): Promise<{ success: boolean }> { await popGitStash(directory, { ref: 'stash@{0}' }); return { success: true }; } export async function getConflictDetails(directory: string): Promise { const response = await runtimeFetch(buildUrl(`${API_BASE}/conflict-details`, directory)); if (!response.ok) { throw new Error(`Failed to get conflict details: ${response.statusText}`); } return response.json(); } export async function validateWorktreeDirectory( directory: string, worktreeRoot: string ): Promise<{ valid: boolean; insideWorktreeRoot: boolean; resolvedWorktreeRoot: string | null; resolvedCwd: string | null; }> { const response = await runtimeFetch(`${API_BASE}/validate-directory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ directory, worktreeRoot }), }); if (!response.ok) { throw new Error(`Failed to validate worktree directory: ${response.statusText}`); } return response.json(); } export async function canonicalizeWorktreeState( directory: string ): Promise<{ worktreeRoot: string | null; cwd: string | null; branch: string | null; headState: 'branch' | 'detached' | 'unborn'; worktreeStatus: 'pending' | 'ready' | 'missing' | 'invalid' | 'not-a-repo'; legacy: boolean; degraded: boolean; attentionReason?: 'merge' | 'rebase' | 'cherry-pick' | 'revert' | 'bisect' | null; }> { const response = await runtimeFetch(`${API_BASE}/canonicalize-worktree-state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ directory }), }); if (!response.ok) { throw new Error(`Failed to canonicalize worktree state: ${response.statusText}`); } return response.json(); }