import { mkdir, rename, unlink, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { gzipSync } from "node:zlib"; const repository = "bitcoinresearchkit/brk"; const revision = process.argv[2] ?? "main"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const output = resolve( process.argv[3] ?? `${root}/website_next/ask/tools/source/catalog.jsonl.gz`, ); const temporaryOutput = `${output}.${process.pid}.tmp`; const maxFileSize = 128_000; const concurrency = 12; const extensions = new Set([ "css", "html", "js", "json", "md", "mjs", "py", "rs", "sh", "toml", "ts", "yaml", "yml", ]); const excludedPrefixes = [ ".git/", ".github/", "docs/ai/", "modules/", "packages/brk_client/brk_client/", "target/", "website/assets/", "website_next/modules/", ]; const excludedFiles = new Set([ "crates/brk_server/src/api/scalar.js", "docs/CHANGELOG.md", "website/scripts/options/scalar.js", ]); const headers = { Accept: "application/vnd.github+json", "User-Agent": "brk-ask-source-builder", }; async function fetchResponse(url, options = {}) { let lastError; for (let attempt = 0; attempt < 3; attempt += 1) { try { const response = await fetch(url, options); if (response.ok) return response; lastError = new Error(`${response.status} ${response.statusText}: ${url}`); if (response.status < 500 && response.status !== 429) break; } catch (error) { lastError = error; } await new Promise((resolveDelay) => setTimeout(resolveDelay, 250 * 2 ** attempt), ); } throw lastError; } async function fetchJson(url) { return fetchResponse(url, { headers }).then((response) => response.json()); } function isUsefulSource(entry) { if (entry.type !== "blob" || !entry.size || entry.size > maxFileSize) { return false; } const extension = entry.path.slice(entry.path.lastIndexOf(".") + 1).toLowerCase(); return ( extensions.has(extension) && !excludedFiles.has(entry.path) && !excludedPrefixes.some((prefix) => entry.path.startsWith(prefix)) ); } function rawUrl(commit, filePath) { const encodedPath = filePath.split("/").map(encodeURIComponent).join("/"); return `https://raw.githubusercontent.com/${repository}/${commit}/${encodedPath}`; } async function mapConcurrent(items, mapper) { const results = new Array(items.length); let nextIndex = 0; async function worker() { while (nextIndex < items.length) { const index = nextIndex; nextIndex += 1; results[index] = await mapper(items[index], index); } } await Promise.all( Array.from({ length: Math.min(concurrency, items.length) }, worker), ); return results; } async function main() { const commitInfo = await fetchJson( `https://api.github.com/repos/${repository}/commits/${encodeURIComponent(revision)}`, ); const commit = commitInfo.sha; if (!commit) throw new Error(`Could not resolve revision: ${revision}`); const treeInfo = await fetchJson( `https://api.github.com/repos/${repository}/git/trees/${commit}?recursive=1`, ); if (treeInfo.truncated) { throw new Error("GitHub returned a truncated repository tree"); } const entries = treeInfo.tree.filter(isUsefulSource); entries.sort((left, right) => left.path.localeCompare(right.path)); const files = await mapConcurrent(entries, async (entry, index) => { const response = await fetchResponse(rawUrl(commit, entry.path)); const source = await response.text(); if ((index + 1) % 250 === 0 || index + 1 === entries.length) { console.error(`Fetched ${index + 1}/${entries.length}`); } return JSON.stringify([entry.path, source]); }); const header = JSON.stringify({ repository, revision: commit, count: files.length, }); const raw = Buffer.from([header, ...files].join("\n")); const compressed = gzipSync(raw, { level: 9 }); await mkdir(dirname(output), { recursive: true }); try { await writeFile(temporaryOutput, compressed); await rename(temporaryOutput, output); } catch (error) { await unlink(temporaryOutput).catch(() => {}); throw error; } console.log( JSON.stringify( { revision: commit, files: files.length, rawBytes: raw.length, compressedBytes: compressed.length, output, }, null, 2, ), ); } await main();