import * as fs from "node:fs/promises"; import type { IncomingMessage, ServerResponse } from "node:http"; import * as path from "node:path"; import { runGit } from "./gitSpawn.js"; import { readJsonBody } from "./httpUtil.js"; import { getCodeRoot, isGitRepo, repoDirForName, repoNameFromGitUrl, } from "./paths.js"; import { copyDefault4nkaiignoreIfMissing } from "./write4nkaiignore.js"; const json = (res: ServerResponse, status: number, body: unknown): void => { res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(body)); }; const isRecord = (v: unknown): v is Record => typeof v === "object" && v !== null && !Array.isArray(v); const readBranch = (body: unknown, fallback: string): string => { if (!isRecord(body)) { return fallback; } const b = body.branch; return typeof b === "string" && b.trim().length > 0 ? b.trim() : fallback; }; const readUrl = (body: unknown): string => { if (!isRecord(body)) { throw new Error("Expected JSON object with url"); } const u = body.url; if (typeof u !== "string" || u.trim().length === 0) { throw new Error("Missing or invalid url"); } return u.trim(); }; const readName = (body: unknown): string => { if (!isRecord(body)) { throw new Error("Expected JSON object with name"); } const n = body.name; if (typeof n !== "string" || n.trim().length === 0) { throw new Error("Missing or invalid name"); } return n.trim(); }; export const handleReposClone = async ( req: IncomingMessage, res: ServerResponse, ): Promise => { const codeRoot = getCodeRoot(); const body = await readJsonBody(req); const url = readUrl(body); const branch = readBranch(body, "test"); const name = repoNameFromGitUrl(url); const dest = path.join(codeRoot, name); try { await fs.access(dest); json(res, 409, { error: "Target directory already exists", name, path: dest, }); return; } catch { /* absent */ } const r = await runGit( ["clone", "-b", branch, "--single-branch", url, dest], codeRoot, ); if (r.code !== 0) { json(res, 500, { error: "git clone failed", code: r.code, stderr: r.stderr, stdout: r.stdout, }); return; } let fourNkAiIgnoreTemplateWrote = false; try { const c = await copyDefault4nkaiignoreIfMissing(dest); fourNkAiIgnoreTemplateWrote = c.wrote; } catch (e) { const msg = e instanceof Error ? e.message : String(e); json(res, 500, { error: "clone ok but failed to write default .4nkaiignore template", detail: msg, name, path: dest, }); return; } json(res, 200, { ok: true, name, path: dest, branch, url, fourNkAiIgnoreTemplateWrote, }); }; export const handleReposList = async (res: ServerResponse): Promise => { const codeRoot = getCodeRoot(); const entries = await fs.readdir(codeRoot, { withFileTypes: true }); const repos: { name: string; path: string }[] = []; for (const ent of entries) { if (!ent.isDirectory()) { continue; } const full = path.join(codeRoot, ent.name); if (await isGitRepo(full)) { repos.push({ name: ent.name, path: full }); } } repos.sort((a, b) => a.name.localeCompare(b.name)); json(res, 200, { repos, codeRoot }); }; export const handleReposLoad = async ( req: IncomingMessage, res: ServerResponse, ): Promise => { const codeRoot = getCodeRoot(); const body = await readJsonBody(req); const name = readName(body); const dest = repoDirForName(codeRoot, name); try { await fs.access(dest); } catch { json(res, 404, { error: "Repository directory not found", name, path: dest }); return; } if (!(await isGitRepo(dest))) { json(res, 400, { error: "Directory is not a git repository", name, path: dest }); return; } json(res, 200, { ok: true, name, path: dest }); };