export interface VersionCommit { id: string message: string authorName: string authorEmail: string committedAt: Date } export interface FileVersion { filePath: string content: string changeType: 'added' | 'modified' | 'deleted' } export interface GitHubRepo { id: number name: string full_name: string owner: { login: string id: number } private: boolean html_url: string clone_url: string ssh_url: string default_branch: string } export interface GitHubCommit { sha: string message: string author: { name: string email: string date: string } url: string html_url: string } export class GitHubService { private baseUrl = 'https://api.github.com' /** * Get user's repositories */ async getUserRepos(accessToken: string): Promise { const response = await this.makeRequest('/user/repos', { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to fetch repositories: ${response.statusText}`) } return await response.json() } /** * Create a new repository */ async createRepo( accessToken: string, name: string, description?: string, isPrivate: boolean = true ): Promise { const response = await this.makeRequest('/user/repos', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ name, description, private: isPrivate, auto_init: true }) }) if (!response.ok) { const error = await response.json() throw new Error(`Failed to create repository: ${error.message}`) } return await response.json() } /** * Get repository information */ async getRepo(accessToken: string, owner: string, repo: string): Promise { const response = await this.makeRequest(`/repos/${owner}/${repo}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to fetch repository: ${response.statusText}`) } return await response.json() } /** * Push a commit to GitHub */ async pushCommit( accessToken: string, owner: string, repo: string, commit: VersionCommit, fileVersions: FileVersion[], branch: string = 'main' ): Promise { try { // Get the current branch reference const branchRef = await this.getBranchRef(accessToken, owner, repo, branch) const parentSha = branchRef.object.sha // Get the current tree const parentTree = await this.getTree(accessToken, owner, repo, parentSha) // Create blobs for changed files const treeChanges = await Promise.all( fileVersions.map(async (fileVersion) => { if (fileVersion.changeType === 'deleted') { return { path: fileVersion.filePath, mode: '100644' as const, type: 'blob' as const, sha: null // null SHA means delete } } else { const blob = await this.createBlob(accessToken, owner, repo, fileVersion.content) return { path: fileVersion.filePath, mode: '100644' as const, type: 'blob' as const, sha: blob.sha } } }) ) // Create new tree const newTree = await this.createTree(accessToken, owner, repo, { base_tree: parentTree.sha, tree: treeChanges }) // Create commit const newCommit = await this.createCommit(accessToken, owner, repo, { message: commit.message, tree: newTree.sha, parents: [parentSha], author: { name: commit.authorName, email: commit.authorEmail, date: commit.committedAt.toISOString() } }) // Update branch reference await this.updateRef(accessToken, owner, repo, `heads/${branch}`, newCommit.sha) return { sha: newCommit.sha, message: newCommit.message, author: newCommit.author, url: newCommit.url, html_url: newCommit.html_url } } catch (error) { console.error('Error pushing commit to GitHub:', error) throw new Error(`Failed to push commit: ${error instanceof Error ? error.message : 'Unknown error'}`) } } /** * Pull latest commits from GitHub */ async pullCommits( accessToken: string, owner: string, repo: string, branch: string = 'main', since?: Date ): Promise { let url = `/repos/${owner}/${repo}/commits?sha=${branch}` if (since) { url += `&since=${since.toISOString()}` } const response = await this.makeRequest(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to fetch commits: ${response.statusText}`) } const commits = await response.json() return commits.map((commit: any) => ({ sha: commit.sha, message: commit.commit.message, author: commit.commit.author, url: commit.url, html_url: commit.html_url })) } /** * Get file content from GitHub */ async getFileContent( accessToken: string, owner: string, repo: string, path: string, ref?: string ): Promise { let url = `/repos/${owner}/${repo}/contents/${path}` if (ref) { url += `?ref=${ref}` } const response = await this.makeRequest(url, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to fetch file content: ${response.statusText}`) } const data = await response.json() return Buffer.from(data.content, 'base64').toString('utf-8') } /** * Create or update a file in GitHub */ async updateFile( accessToken: string, owner: string, repo: string, path: string, content: string, message: string, branch?: string, sha?: string ): Promise { const body: any = { message, content: Buffer.from(content).toString('base64') } if (branch) body.branch = branch if (sha) body.sha = sha const response = await this.makeRequest(`/repos/${owner}/${repo}/contents/${path}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) if (!response.ok) { const error = await response.json() throw new Error(`Failed to update file: ${error.message}`) } return await response.json() } /** * Create a release/tag */ async createRelease( accessToken: string, owner: string, repo: string, tagName: string, name: string, body?: string, draft: boolean = false, prerelease: boolean = false ): Promise { const response = await this.makeRequest(`/repos/${owner}/${repo}/releases`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ tag_name: tagName, name, body, draft, prerelease }) }) if (!response.ok) { const error = await response.json() throw new Error(`Failed to create release: ${error.message}`) } return await response.json() } // Private helper methods private async makeRequest(endpoint: string, options: RequestInit = {}): Promise { const url = endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}` return fetch(url, { ...options, headers: { 'User-Agent': 'BookWiz-App', ...options.headers } }) } private async getBranchRef(accessToken: string, owner: string, repo: string, branch: string) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/refs/heads/${branch}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to get branch reference: ${response.statusText}`) } return await response.json() } private async getTree(accessToken: string, owner: string, repo: string, sha: string) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/trees/${sha}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json' } }) if (!response.ok) { throw new Error(`Failed to get tree: ${response.statusText}`) } return await response.json() } private async createBlob(accessToken: string, owner: string, repo: string, content: string) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/blobs`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ content: Buffer.from(content).toString('base64'), encoding: 'base64' }) }) if (!response.ok) { throw new Error(`Failed to create blob: ${response.statusText}`) } return await response.json() } private async createTree(accessToken: string, owner: string, repo: string, treeData: any) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/trees`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify(treeData) }) if (!response.ok) { throw new Error(`Failed to create tree: ${response.statusText}`) } return await response.json() } private async createCommit(accessToken: string, owner: string, repo: string, commitData: any) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/commits`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify(commitData) }) if (!response.ok) { throw new Error(`Failed to create commit: ${response.statusText}`) } return await response.json() } private async updateRef(accessToken: string, owner: string, repo: string, ref: string, sha: string) { const response = await this.makeRequest(`/repos/${owner}/${repo}/git/refs/${ref}`, { method: 'PATCH', headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/vnd.github.v3+json', 'Content-Type': 'application/json' }, body: JSON.stringify({ sha }) }) if (!response.ok) { throw new Error(`Failed to update reference: ${response.statusText}`) } return await response.json() } } export const githubService = new GitHubService()