/** * Gitea API integration * * Handles repo creation, file scaffolding, and webhook registration * for user projects. All project repos are created under the user's * Gitea account and contain the full Turborepo monorepo structure. */ import { config } from "./config.js"; import { readdir, readFile } from "node:fs/promises"; import { join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { dirname } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const TEMPLATES_DIR = join(__dirname, "../../../../scripts/templates/turborepo"); type GiteaFile = { path: string; content: string; }; async function giteaFetch(path: string, options: RequestInit = {}): Promise { const url = `${config.giteaUrl}/api/v1${path}`; const res = await fetch(url, { ...options, headers: { "Authorization": `token ${config.giteaToken}`, "Content-Type": "application/json", ...options.headers, }, }); return res; } export async function createRepo(owner: string, repoName: string, description: string): Promise { const res = await giteaFetch(`/user/repos`, { method: "POST", body: JSON.stringify({ name: repoName, description, private: false, auto_init: false, }), }); if (!res.ok) { const body = await res.text(); throw new Error(`Failed to create Gitea repo: ${res.status} ${body}`); } const data = await res.json() as { clone_url: string }; return data.clone_url; } export async function registerWebhook(owner: string, repoName: string, webhookUrl: string): Promise { const res = await giteaFetch(`/repos/${owner}/${repoName}/hooks`, { method: "POST", body: JSON.stringify({ type: "gitea", active: true, events: ["push", "pull_request"], config: { url: webhookUrl, content_type: "json", }, }), }); if (!res.ok) { const body = await res.text(); throw new Error(`Failed to register webhook: ${res.status} ${body}`); } } /** * Walk the template directory and collect all files with their content, * replacing {{project-slug}} and {{project-name}} placeholders. */ async function collectTemplateFiles( projectSlug: string, projectName: string ): Promise { const files: GiteaFile[] = []; async function walk(dir: string) { const entries = await readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dir, entry.name); if (entry.isDirectory()) { await walk(fullPath); } else { const relPath = relative(TEMPLATES_DIR, fullPath); let content = await readFile(fullPath, "utf-8"); content = content .replaceAll("{{project-slug}}", projectSlug) .replaceAll("{{project-name}}", projectName); files.push({ path: relPath, content }); } } } await walk(TEMPLATES_DIR); return files; } /** * Push the full Turborepo scaffold to a Gitea repo as an initial commit. * Uses Gitea's contents API to create each file individually. */ export async function scaffoldRepo( owner: string, repoName: string, projectSlug: string, projectName: string ): Promise { const files = await collectTemplateFiles(projectSlug, projectName); for (const file of files) { const encoded = Buffer.from(file.content).toString("base64"); const res = await giteaFetch(`/repos/${owner}/${repoName}/contents/${file.path}`, { method: "POST", body: JSON.stringify({ message: `chore: scaffold ${file.path}`, content: encoded, branch: "main", }), }); if (!res.ok) { const body = await res.text(); throw new Error(`Failed to push ${file.path}: ${res.status} ${body}`); } } } export async function getRepoTree(owner: string, repoName: string, ref = "main"): Promise { const res = await giteaFetch(`/repos/${owner}/${repoName}/git/trees/${ref}?recursive=true`); if (!res.ok) return []; const data = await res.json() as { tree: { path: string; type: string }[] }; return data.tree.filter(e => e.type === "blob").map(e => e.path); } export async function getFileContent( owner: string, repoName: string, filePath: string, ref = "main" ): Promise { const res = await giteaFetch(`/repos/${owner}/${repoName}/contents/${filePath}?ref=${ref}`); if (!res.ok) return null; const data = await res.json() as { content: string }; return Buffer.from(data.content, "base64").toString("utf-8"); }