import type { GitStatus, GitDiffResponse, GetGitDiffOptions, GitFileDiffResponse, GetGitFileDiffOptions, GitBranch, GitDeleteBranchPayload, GitDeleteRemoteBranchPayload, GeneratedCommitMessage, GitWorktreeInfo, GitAddWorktreePayload, GitRemoveWorktreePayload, CreateGitCommitOptions, GitCommitResult, GitPushResult, GitPullResult, GitLogOptions, GitLogResponse, GitCommitFilesResponse, GitIdentityProfile, GitIdentitySummary, DiscoveredGitCredential, } from './api/types'; declare global { interface Window { __OPENCHAMBER_DESKTOP_SERVER__?: { origin: string; opencodePort: number | null; apiPrefix: string; cliAvailable: boolean; }; } } const resolveBaseOrigin = (): string => { if (typeof window === 'undefined') { return ''; } const desktopOrigin = window.__OPENCHAMBER_DESKTOP_SERVER__?.origin; if (desktopOrigin) { return desktopOrigin; } return window.location.origin; }; const API_BASE = '/api/git'; function buildUrl( path: string, directory: string | null | undefined, params?: Record ): string { const url = new URL(path, resolveBaseOrigin()); if (directory) { url.searchParams.set('directory', directory); } if (params) { for (const [key, value] of Object.entries(params)) { if (value === undefined) continue; url.searchParams.set(key, String(value)); } } return url.toString(); } export async function checkIsGitRepository(directory: string): Promise { const response = await fetch(buildUrl(`${API_BASE}/check`, directory)); if (!response.ok) { throw new Error(`Failed to check git repository: ${response.statusText}`); } const data = await response.json(); return data.isGitRepository; } export async function getGitStatus(directory: string): Promise { const response = await fetch(buildUrl(`${API_BASE}/status`, directory)); if (!response.ok) { throw new Error(`Failed to get git status: ${response.statusText}`); } return response.json(); } 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 fetch( 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 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 fetch( 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): Promise { if (!filePath) { throw new Error('path is required to revert git changes'); } const response = await fetch(buildUrl(`${API_BASE}/revert`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: filePath }), }); if (!response.ok) { const message = await response .json() .catch(() => ({ error: response.statusText })); throw new Error(message.error || 'Failed to revert git changes'); } } export async function isLinkedWorktree(directory: string): Promise { if (!directory) { return false; } const response = await fetch(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 fetch(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 fetch(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 fetch(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 generateCommitMessage( directory: string, files: string[] ): Promise<{ message: GeneratedCommitMessage }> { if (!Array.isArray(files) || files.length === 0) { throw new Error('No files provided to generate commit message'); } const response = await fetch(buildUrl(`${API_BASE}/commit-message`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to generate commit message'); } 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 } ): Promise<{ title: string; body: string }> { const { base, head } = payload; if (!base || !head) { throw new Error('base and head are required'); } const response = await fetch(buildUrl(`${API_BASE}/pr-description`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ base, head }), }); 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 fetch(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 addGitWorktree(directory: string, payload: GitAddWorktreePayload): Promise<{ success: boolean; path: string; branch: string }> { if (!payload?.path || !payload?.branch) { throw new Error('path and branch are required to add a worktree'); } const response = await fetch(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 add worktree'); } return response.json(); } export async function removeGitWorktree(directory: string, payload: GitRemoveWorktreePayload): Promise<{ success: boolean }> { if (!payload?.path) { throw new Error('path is required to remove a worktree'); } const response = await fetch(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 remove worktree'); } return response.json(); } export async function ensureOpenChamberIgnored(directory: string): Promise { const response = await fetch(buildUrl(`${API_BASE}/ignore-openchamber`, directory), { method: 'POST', }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to update git ignore'); } } export async function createGitCommit( directory: string, message: string, options: CreateGitCommitOptions = {} ): Promise { const response = await fetch(buildUrl(`${API_BASE}/commit`, directory), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, addAll: options.addAll ?? false, files: options.files, }), }); if (!response.ok) { const error = await response.json().catch(() => ({ error: response.statusText })); throw new Error(error.error || 'Failed to create commit'); } return response.json(); } export async function gitPush( directory: string, options: { remote?: string; branch?: string; options?: string[] | Record } = {} ): Promise { const response = await fetch(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'); } return response.json(); } export async function gitPull( directory: string, options: { remote?: string; branch?: string } = {} ): Promise { const response = await fetch(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'); } return response.json(); } export async function gitFetch( directory: string, options: { remote?: string; branch?: string } = {} ): Promise<{ success: boolean }> { const response = await fetch(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'); } return response.json(); } export async function checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> { const response = await fetch(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 fetch(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 fetch(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 fetch( buildUrl(`${API_BASE}/log`, directory, { maxCount: options.maxCount, from: options.from, to: options.to, file: options.file, }) ); if (!response.ok) { throw new Error(`Failed to get git log: ${response.statusText}`); } return response.json(); } export async function getCommitFiles( directory: string, hash: string ): Promise { const response = await fetch( 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 getGitIdentities(): Promise { const response = await fetch(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 fetch(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 fetch(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 fetch(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 fetch(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 fetch(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 fetch(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 fetch(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 fetch(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 fetch(buildUrl(`${API_BASE}/remote-url`, directory, { remote })); if (!response.ok) { return null; } const data = await response.json(); return data.url ?? null; }