export function daysAgo(n: number): string { const d = new Date(); d.setDate(d.getDate() - n); return d.toISOString().split('T')[0]; } export function log(...args: unknown[]) { console.error(...args); } export function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } export async function ghSearch(query: string): Promise { const url = `https://api.github.com/search/repositories` + `?q=${encodeURIComponent(query)}&sort=stars&order=desc&per_page=30`; log(` fetching: ${query}`); const res = await fetch(url, { headers: { Accept: 'application/vnd.github.v3+json' }, }); if (!res.ok) { log(` ⚠ GitHub API ${res.status}: ${await res.text()}`); return []; } const data = (await res.json()) as { items: GHRepo[] }; return data.items ?? []; } export interface GHRepo { full_name: string; description: string | null; stargazers_count: number; language: string | null; html_url: string; created_at: string; pushed_at: string; topics?: string[]; } export function formatRepos(repos: GHRepo[], title: string): string { if (!repos.length) return `## ${title}\n\n_No results found._\n`; const lines = [`## ${title}\n`]; for (const r of repos) { const desc = r.description ? r.description.slice(0, 120) : '_No description_'; const lang = r.language ?? '?'; lines.push( `**[${r.full_name}](${r.html_url})** ` + `⭐ ${r.stargazers_count.toLocaleString()} | \`${lang}\`` ); lines.push(`> ${desc}\n`); } return lines.join('\n'); } export function dedupeRepos(repos: GHRepo[]): GHRepo[] { const seen = new Set(); return repos.filter((r) => { if (seen.has(r.full_name)) return false; seen.add(r.full_name); return true; }); }