import { execFile } from "node:child_process"; import { access, readFile, readdir, stat } from "node:fs/promises"; import { extname, join, relative, resolve } from "node:path"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); const README_NAMES = ["README.md", "readme.md", "Readme.md", "README", "readme", "README.rst", "README.txt"]; const LICENSE_NAMES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "LICENCE.txt"]; const SOURCE_LANGUAGES = new Map([ [".cs", "C#"], [".fs", "F#"], [".vb", "VB.NET"], [".py", "Python"], [".rs", "Rust"], [".go", "Go"], [".ts", "TypeScript"], [".tsx", "TypeScript"], [".js", "JavaScript"], [".jsx", "JavaScript"], [".mjs", "JavaScript"], [".java", "Java"], [".kt", "Kotlin"], [".swift", "Swift"], [".rb", "Ruby"], [".cpp", "C++"], [".cc", "C++"], [".c", "C"], [".zig", "Zig"], [".lua", "Lua"], [".php", "PHP"], [".dart", "Dart"], [".ex", "Elixir"], [".exs", "Elixir"], [".hs", "Haskell"], [".scala", "Scala"], [".r", "R"], [".jl", "Julia"], [".ps1", "PowerShell"], [".sh", "Shell"], ]); const SKIPPED_DIRECTORIES = new Set([ ".git", ".next", ".nuxt", ".output", ".turbo", "bin", "build", "coverage", "dist", "node_modules", "obj", "out", "target", "vendor", ]); const BAD_IMAGE_HOSTS = new Set([ "avatars.githubusercontent.com", "badge.fury.io", "contrib.rocks", "img.shields.io", "opencollective.com", ]); const THEMES = [ "None (site default)", "terminal", "neon", "minimal", "pastel", "matrix", "sunset", "ocean", "forest", "candy", "synthwave", "newspaper", "retro", ]; async function run(file, args, cwd, timeout = 15000) { const { stdout } = await execFileAsync(file, args, { cwd, timeout, maxBuffer: 1024 * 1024, windowsHide: true, }); return stdout.trim(); } async function runOptional(file, args, cwd, timeout) { try { return await run(file, args, cwd, timeout); } catch { return ""; } } async function pathExists(path) { try { await access(path); return true; } catch { return false; } } async function findFirst(root, names) { let rootEntries; for (const name of names) { if (name.startsWith("*.")) { rootEntries ??= await readdir(root, { withFileTypes: true }); const suffix = name.slice(1).toLowerCase(); const match = rootEntries.find((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(suffix)); if (match) { return join(root, match.name); } continue; } const path = join(root, name); if (await pathExists(path)) { return path; } } return ""; } function parseJson(value, fallback = null) { try { return JSON.parse(value); } catch { return fallback; } } function normalizeGitUrl(value) { const url = String(value || "").trim(); if (url.startsWith("git@github.com:")) { return `https://github.com/${url.slice("git@github.com:".length).replace(/\.git$/i, "")}`; } try { const parsed = new URL(url); if (parsed.hostname.toLowerCase() !== "github.com") return url; const segments = parsed.pathname.replace(/\.git$/i, "").split("/").filter(Boolean); if (segments.length < 2) return url; return `https://github.com/${segments[0]}/${segments[1]}`; } catch { return url.replace(/\.git$/i, "").replace(/\/$/, ""); } } function githubCoordinates(githubUrl) { const match = String(githubUrl || "").match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+)$/i); return match ? { owner: match[1], repo: match[2] } : null; } function cleanMarkdown(text) { return String(text || "") .replace(//g, " ") .replace(/!\[[^\]]*]\([^)]*\)/g, " ") .replace(/\[([^\]]+)]\([^)]*\)/g, "$1") .replace(/`{1,3}[^`]*`{1,3}/g, " ") .replace(/[*_>#|~-]/g, " ") .replace(/\s+/g, " ") .trim(); } function firstReadmeParagraph(readme) { for (const block of String(readme || "").split(/\r?\n\s*\r?\n/)) { const trimmed = block.trim(); if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("![") || trimmed.startsWith("[![")) { continue; } const cleaned = cleanMarkdown(trimmed); if (cleaned.length >= 30) { return cleaned.slice(0, 600); } } return ""; } function sentenceDescription(name, source) { const cleaned = cleanMarkdown(source); if (!cleaned) { return `${name} is an open source tool built to make a focused task simpler and more delightful. Explore the repository for installation and usage details.`; } const first = /[.!?]$/.test(cleaned) ? cleaned : `${cleaned}.`; return `${first} The project is free and open source, with its source and usage details available on GitHub.`; } function truncate(value, maxLength) { const text = cleanMarkdown(value); if (text.length <= maxLength) { return text; } return `${text.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`; } function detectLicenseText(content) { const text = String(content || "").toLowerCase(); if (text.includes("mit license") || text.includes("licensed under the mit")) return "MIT"; if (text.includes("apache license") || text.includes("apache-2.0")) return "Apache-2.0"; if (text.includes("gnu general public license") || text.includes("gpl-3.0") || text.includes("gplv3")) return "GPL-3.0"; if (text.includes("gpl-2.0") || text.includes("gplv2")) return "GPL-2.0"; if (text.includes("bsd 2-clause")) return "BSD-2-Clause"; if (text.includes("bsd 3-clause")) return "BSD-3-Clause"; if (text.includes("mozilla public license") || text.includes("mpl-2.0")) return "MPL-2.0"; if (text.includes("isc license")) return "ISC"; if (text.includes("the unlicense")) return "Unlicense"; return ""; } async function scanLanguages(root) { const counts = new Map(); let visited = 0; async function visit(directory, depth) { if (depth > 6 || visited > 12000) return; let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch { return; } for (const entry of entries) { if (visited > 12000) break; if (entry.isDirectory()) { if (!SKIPPED_DIRECTORIES.has(entry.name)) { await visit(join(directory, entry.name), depth + 1); } continue; } visited += 1; const language = SOURCE_LANGUAGES.get(extname(entry.name).toLowerCase()); if (language) { counts.set(language, (counts.get(language) || 0) + 1); } } } await visit(root, 0); return [...counts.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] || ""; } function isLikelyBadge(url) { try { const parsed = new URL(url); const hostname = parsed.hostname.toLowerCase(); const pathname = parsed.pathname.toLowerCase(); return BAD_IMAGE_HOSTS.has(hostname) || hostname.endsWith(".githubusercontent.com") && hostname.startsWith("avatars.") || pathname.endsWith(".svg") || pathname.includes("/contributors/") || pathname.includes("/sponsors/"); } catch { return false; } } function normalizeImageUrl(rawUrl, githubUrl, defaultBranch, readmePath, root) { const value = String(rawUrl || "").trim().replace(/^<|>$/g, "").split(/\s+["']/)[0]; if (!value || value.startsWith("data:")) return ""; if (/^https?:\/\//i.test(value)) return isLikelyBadge(value) ? "" : value; if (!githubUrl || value.startsWith("#")) return ""; const relativeReadmeDirectory = value.startsWith("/") ? "" : relative(root, resolve(readmePath, "..")).replaceAll("\\", "/"); const imagePath = [relativeReadmeDirectory, value] .filter(Boolean) .join("/") .replace(/^\.\//, "") .replace(/^\/+/, "") .replaceAll("\\", "/"); return `${githubUrl.replace("github.com", "raw.githubusercontent.com")}/${defaultBranch}/${imagePath}`; } function detectThumbnail(readme, githubUrl, defaultBranch, readmePath, root) { const candidates = []; const imagePattern = /!\[(?[^\]]*)]\((?[^)]+)\)/g; for (const match of readme.matchAll(imagePattern)) { candidates.push({ index: match.index, url: match.groups?.url, alt: match.groups?.alt, }); } const htmlImagePattern = /]*>/gi; for (const match of readme.matchAll(htmlImagePattern)) { const tag = match[0]; const srcMatch = tag.match(/\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s>]+))/i); const altMatch = tag.match(/\balt\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i); if (srcMatch) { candidates.push({ index: match.index, url: srcMatch[1] || srcMatch[2] || srcMatch[3], alt: altMatch ? altMatch[1] || altMatch[2] || altMatch[3] || "" : "", }); } } candidates.sort((left, right) => left.index - right.index); for (const candidate of candidates) { const url = normalizeImageUrl(candidate.url, githubUrl, defaultBranch, readmePath, root); if (url) { return { url, alt: String(candidate.alt || "").trim() }; } } return { url: "", alt: "" }; } function detectWebsite(readme, githubUrl, packageJson, githubInfo) { const candidates = [packageJson?.homepage, githubInfo?.homepageUrl]; const markdownLinks = [...String(readme || "").matchAll(/\[(?