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<GitHubRepo[]> {
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<GitHubRepo> {
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<GitHubRepo> {
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<GitHubCommit> {
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<GitHubCommit[]> {
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<string> {
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<any> {
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<any> {
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<Response> {
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()