godcall/tools/import-gitea.mjs
type-two 260f02915f GODCALL fork: English UI+prompts, http Gitea fetcher, self-buildable images
- Translate panel UI (~500 strings) and all extraction/wiki prompts to English;
  rebrand to GODCALL (title, logo, login, header)
- git-fetcher: allow http:// clone URLs behind KNOWLEDGE_ALLOW_HTTP=1 (tailnet Gitea)
- MemoryCore/Dockerfile: drop-in image (upstream ships none) with
  TDAI_GATEWAY_CONFIG env + healthcheck; combined-hub Dockerfile: stock Debian
  mirrors; stub MemoryKnowledge openapi.yaml (absent upstream but COPYed)
- start-memory-hub.sh: optional GIT_CREDENTIALS_FILE mount (keeps tokens out of
  repo URLs); tools/import-gitea.mjs: bulk CodeGraph import of all Gitea repos

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 21:13:08 +10:00

88 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* GODCALL — bulk-import every Gitea repo as a CodeGraph.
*
* Enumerates repos via the Gitea API and registers each with the knowledge
* service. Clone auth is handled by the git credential store mounted into the
* memory-hub container (see start-memory-hub.sh GIT_CREDENTIALS_FILE) — tokens
* never go into repo URLs.
*
* Env:
* GITEA_URL e.g. http://100.71.119.27:3000
* GITEA_TOKEN_FILE file containing a Gitea access token (read:repository)
* KNOWLEDGE_URL e.g. http://100.91.239.7:8424
* TEAM_ID godcall team id (team-...)
* USER_ID godcall user id (usr-...)
* GITEA_CLONE_BASE optional override when Gitea ROOT_URL differs from the
* address reachable from the hub container
* ONLY optional comma-separated repo names to import
* SKIP optional comma-separated repo names to skip
*/
import { readFileSync } from "node:fs";
const need = (k) => {
const v = process.env[k];
if (!v) { console.error(`missing env ${k}`); process.exit(1); }
return v;
};
const GITEA_URL = need("GITEA_URL").replace(/\/$/, "");
const TOKEN = readFileSync(need("GITEA_TOKEN_FILE"), "utf8").trim();
const KNOWLEDGE_URL = need("KNOWLEDGE_URL").replace(/\/$/, "");
const TEAM_ID = need("TEAM_ID");
const USER_ID = need("USER_ID");
const CLONE_BASE = (process.env.GITEA_CLONE_BASE || GITEA_URL).replace(/\/$/, "");
const ONLY = (process.env.ONLY || "").split(",").map(s => s.trim()).filter(Boolean);
const SKIP = new Set((process.env.SKIP || "").split(",").map(s => s.trim()).filter(Boolean));
async function giteaRepos() {
const repos = [];
for (let page = 1; ; page++) {
const r = await fetch(`${GITEA_URL}/api/v1/repos/search?limit=50&page=${page}`, {
headers: { Authorization: `token ${TOKEN}` },
});
if (!r.ok) throw new Error(`gitea search ${r.status}: ${await r.text()}`);
const { data } = await r.json();
if (!data?.length) break;
repos.push(...data);
if (data.length < 50) break;
}
return repos;
}
async function createCodeGraph(repo) {
const repoUrl = `${CLONE_BASE}/${repo.full_name}.git`;
const body = {
team_id: TEAM_ID,
user_id: USER_ID,
repo_url: repoUrl,
branch: repo.default_branch || "main",
repo_name: repo.name,
};
const r = await fetch(`${KNOWLEDGE_URL}/v3/code-graph/create`, {
method: "POST",
headers: { "Content-Type": "application/json", "x-tdai-service-id": "default" },
body: JSON.stringify(body),
});
const json = await r.json().catch(() => ({}));
return { status: r.status, id: json?.data?.code_graph_id, msg: json?.message };
}
const repos = await giteaRepos();
console.log(`gitea reports ${repos.length} repos`);
let ok = 0, fail = 0;
for (const repo of repos) {
if (ONLY.length && !ONLY.includes(repo.name)) continue;
if (SKIP.has(repo.name)) { console.log(`skip ${repo.full_name}`); continue; }
try {
const res = await createCodeGraph(repo);
const good = res.status === 200 || res.status === 201;
good ? ok++ : fail++;
console.log(`${good ? "ok " : "FAIL"} ${repo.full_name} [${repo.default_branch}] -> ${res.id ?? res.msg ?? res.status}`);
} catch (e) {
fail++;
console.log(`FAIL ${repo.full_name}: ${e.message}`);
}
}
console.log(`done: ${ok} registered, ${fail} failed`);