| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192 |
- import { formatCount, type RepoMeta } from "@pipeline/shared";
- /**
- * Shared single-repo detail helpers — the fetch + format logic behind the
- * github-repo collector, parameterized by README truncation length so
- * github-daily-pick can ask for a deeper excerpt without duplicating code.
- */
- /** Format a repo-detail API payload as markdown (the LLM's fact source),
- * plus the typed RepoMeta the text module treats as authoritative. */
- export function formatRepoDetail(
- queryOwner: string,
- queryRepo: string,
- data: any,
- readmeLimit = 3000,
- ): { text: string; meta: RepoMeta | null } {
- // The API returns the REAL owner (transferred repos report their current
- // owner). Prefer data.fullName; fall back to the query params.
- const fullName = data.fullName || `${queryOwner}/${queryRepo}`;
- const [detailOwner, detailName] = fullName.split("/");
- const meta: RepoMeta = {
- owner: detailOwner || queryOwner,
- name: detailName || queryRepo,
- fullName,
- language: data.language ?? "",
- languageColor: data.languageColor ?? "",
- stars: data.stars,
- forks: data.forks,
- license: data.license ?? "",
- };
- const lines: string[] = [`# ${fullName}\n`];
- if (data.description) lines.push(`${data.description}\n`);
- // Structured metadata block — LLM copies verbatim into scene.github.repo.
- lines.push(`<!-- repo-meta: ${JSON.stringify(meta)} -->`);
- const meta2: string[] = [];
- if (data.language) meta2.push(`Language: ${data.language}`);
- if (data.stars != null) meta2.push(`Stars: ${formatCount(data.stars)}`);
- if (data.forks != null) meta2.push(`Forks: ${formatCount(data.forks)}`);
- if (data.openIssues != null) meta2.push(`Open Issues: ${formatCount(data.openIssues)}`);
- if (data.topics?.length) meta2.push(`Topics: ${data.topics.join(", ")}`);
- if (meta2.length) {
- lines.push("");
- lines.push(meta2.map((m) => `- ${m}`).join("\n"));
- }
- if (data.readme) {
- const readme = data.readme.length > readmeLimit
- ? data.readme.slice(0, readmeLimit) + "\n..."
- : data.readme;
- lines.push("\n## README\n", readme);
- }
- return { text: lines.join("\n"), meta };
- }
- /** Fetch one repo's detail payload and format it. Returns null on any failure
- * so callers can decide how to degrade. Retries transient upstream errors
- * (5xx / network) up to 2 times with a short backoff — a blip shouldn't burn
- * the daily pick's topic. */
- export async function fetchRepoDetail(
- repoUrlTemplate: string,
- owner: string,
- repo: string,
- readmeLimit = 3000,
- ): Promise<{ text: string; meta: RepoMeta | null } | null> {
- if (!repoUrlTemplate) return null;
- const url = repoUrlTemplate
- .replace(":owner", encodeURIComponent(owner))
- .replace(":repo", encodeURIComponent(repo));
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
- for (let attempt = 0; attempt < 3; attempt++) {
- try {
- const response = await fetch(url);
- if (response.ok) {
- const raw = (await response.json()) as Record<string, any>;
- const data = (raw.data ?? raw) as Record<string, any>;
- return formatRepoDetail(owner, repo, data, readmeLimit);
- }
- // 4xx is a real answer (repo gone etc.) — do not retry.
- if (response.status < 500) return null;
- } catch {
- // network error — retry
- }
- if (attempt < 2) await sleep(1000 * (attempt + 1));
- }
- return null;
- }
|