import type { GitHost } from './git-host'; import { uint8ArrayToBase64, base64ToUint8Array } from './git-host'; /// GitHub Contents API implementation. /// /// Endpoints: /// GET https://api.github.com/repos/{repoPath}/contents/{path} /// PUT https://api.github.com/repos/{repoPath}/contents/{path} (create/update) /// DELETE https://api.github.com/repos/{repoPath}/contents/{path} /// /// Auth: `Bearer {apiToken}` header. /// Content is base64-encoded. Updates and deletes require the current file SHA. export class GitHubHost implements GitHost { private baseUrl: string; private headers: Record; constructor(repoPath: string, apiToken: string) { this.baseUrl = `https://api.github.com/repos/${repoPath}/contents`; this.headers = { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.v3+json', 'X-GitHub-Api-Version': '2022-11-28', }; } async readFile(path: string): Promise { const resp = await fetch(`${this.baseUrl}/${path}`, { headers: this.headers, }); if (!resp.ok) { throw new Error(`GitHub readFile ${path}: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); const clean = (json.content as string).replace(/\n/g, ''); return base64ToUint8Array(clean); } async writeFile(path: string, content: Uint8Array, message: string): Promise { const b64 = uint8ArrayToBase64(content); // Try to get the current SHA for an update; if 404 it's a create. let sha: string | null = null; try { const existing = await fetch(`${this.baseUrl}/${path}`, { headers: this.headers, }); if (existing.ok) { const json = await existing.json(); sha = json.sha as string; } } catch { // File does not exist — will create. } const body: Record = { content: b64, message }; if (sha) { body.sha = sha; } const resp = await fetch(`${this.baseUrl}/${path}`, { method: 'PUT', headers: this.headers, body: JSON.stringify(body), }); if (!resp.ok) { const text = await resp.text(); throw new Error(`GitHub writeFile ${path}: ${resp.status} ${text}`); } } async deleteFile(path: string, message: string): Promise { const existing = await fetch(`${this.baseUrl}/${path}`, { headers: this.headers, }); if (!existing.ok) { throw new Error(`GitHub deleteFile ${path}: file not found (${existing.status})`); } const json = await existing.json(); const sha = json.sha as string; const resp = await fetch(`${this.baseUrl}/${path}`, { method: 'DELETE', headers: this.headers, body: JSON.stringify({ message, sha }), }); if (!resp.ok) { const text = await resp.text(); throw new Error(`GitHub deleteFile ${path}: ${resp.status} ${text}`); } } async listDir(path: string): Promise { const resp = await fetch(`${this.baseUrl}/${path}`, { headers: this.headers, }); if (!resp.ok) { if (resp.status === 404) return []; throw new Error(`GitHub listDir ${path}: ${resp.status} ${resp.statusText}`); } const json = await resp.json(); if (!Array.isArray(json)) return []; return json.map((item: { name: string }) => item.name); } }