#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; import { fileURLToPath } from "node:url"; function values(name, short) { const out = []; const a = process.argv; for (let i = 2; i < a.length; i++) if (a[i] === name || (short && a[i] === short)) out.push(a[++i]); return out; } function arg(name, short, def) { const v = values(name, short); return v.length ? v.at(-1) : def; } function has(name) { return process.argv.includes(name); } function positional() { return process.argv .slice(2) .find( (a, i, arr) => !a.startsWith("-") && ![ "--limit", "-L", "--language", "--filename", "--extension", "--repo", "-R", "--owner", "--match", "--size", "--min-matches", "--output", "-o", "--sort-by", "--output-file", "--timeout", ].includes(arr[i - 1]), ); } function checkGh() { const r = spawnSync("gh", ["auth", "status"], { stdio: "ignore" }); return !r.error && r.status === 0; } function build(query) { const cmd = ["gh", "search", "code", query]; const add = (flag, value) => { if (value) cmd.push(flag, String(value)); }; add("--limit", arg("--limit", "-L", "30")); add("--language", arg("--language")); add("--filename", arg("--filename")); add("--extension", arg("--extension")); for (const r of values("--repo", "-R")) cmd.push("--repo", r); for (const o of values("--owner")) cmd.push("--owner", o); add("--match", arg("--match")); add("--size", arg("--size")); cmd.push("--json", "path,repository,sha,textMatches,url"); return cmd; } function execute(cmd, timeout) { const r = spawnSync(cmd[0], cmd.slice(1), { encoding: "utf8", timeout: timeout * 1000, maxBuffer: 64 * 1024 * 1024, }); if (r.error?.code === "ETIMEDOUT") throw new Error( `Search timed out after ${timeout} seconds. Try a simpler query or increase --timeout.`, ); if (r.status !== 0) { const e = (r.stderr ?? "").trim(); if (e.includes("HTTP 403") && e.toLowerCase().includes("rate limit")) throw new Error( `GitHub API rate limit exceeded. Please wait and try again later.\nDetails: ${e}`, ); if (e.includes("HTTP 408")) throw new Error( `Search query timed out. Try a simpler query or try again later.\nDetails: ${e}`, ); throw new Error(`GitHub search failed: ${e}`); } return r.stdout.trim() ? JSON.parse(r.stdout) : []; } function filter(results) { let out = results; if (has("--exclude-forks")) out = out.filter((r) => !r.repository?.isFork); if (has("--exclude-private")) out = out.filter((r) => !r.repository?.isPrivate); const min = Number(arg("--min-matches", undefined, "0")); if (min) out = out.filter((r) => (r.textMatches ?? []).length >= min); return out; } function sort(results, by) { if (by === "matches") return results.toSorted( (a, b) => (b.textMatches ?? []).length - (a.textMatches ?? []).length, ); if (by === "repo") return results.toSorted((a, b) => (a.repository?.nameWithOwner ?? "").localeCompare( b.repository?.nameWithOwner ?? "", ), ); if (by === "path") return results.toSorted((a, b) => (a.path ?? "").localeCompare(b.path ?? ""), ); return results; } function pretty(results) { if (!results.length) return "No results found."; const out = [`Found ${results.length} result(s)\n`, "=".repeat(80)]; results.forEach((r, i) => { let frag = r.textMatches?.[0]?.fragment ?? ""; if (frag.length > 100) frag = `${frag.slice(0, 97)}...`; out.push( `\n${i + 1}. ${r.repository?.nameWithOwner ?? "Unknown"}:${r.path ?? "Unknown"}`, ` URL: ${r.url ?? ""}`, ` Matches: ${(r.textMatches ?? []).length}`, ); if (frag) out.push(` Preview: ${frag}`); out.push("-".repeat(80)); }); return out.join("\n"); } function summary(results) { if (!results.length) return "No results found."; const repos = {}, exts = {}; let total = 0; for (const r of results) { const repo = r.repository?.nameWithOwner ?? "Unknown"; repos[repo] = (repos[repo] ?? 0) + 1; total += (r.textMatches ?? []).length; const p = r.path ?? ""; if (p.includes(".")) { const e = p.split(".").at(-1); exts[e] = (exts[e] ?? 0) + 1; } } const out = [ "SEARCH SUMMARY", "=".repeat(80), `Total files found: ${results.length}`, `Total text matches: ${total}`, `Unique repositories: ${Object.keys(repos).length}`, "\nTop Repositories:", ]; for (const [k, v] of Object.entries(repos) .sort((a, b) => b[1] - a[1]) .slice(0, 10)) out.push(` ${k}: ${v} file(s)`); out.push("\nFile Extensions:"); for (const [k, v] of Object.entries(exts).sort((a, b) => b[1] - a[1])) out.push(` .${k}: ${v} file(s)`); return out.join("\n"); } function main() { if (!checkGh()) { console.error( "Error: GitHub CLI (gh) is not installed or not authenticated.", ); console.error("Install from: https://cli.github.com/"); console.error("Then run: gh auth login"); return 1; } const q = positional(); if (!q) { console.error("Error: query required"); return 1; } const results = sort( filter(execute(build(q), Number(arg("--timeout", undefined, "30")))), arg("--sort-by"), ); const fmt = arg("--output", "-o", "pretty"); const output = fmt === "json" ? JSON.stringify(results, null, 2) : fmt === "summary" ? summary(results) : pretty(results); const file = arg("--output-file"); if (file) { mkdirSync(dirname(file), { recursive: true }); writeFileSync(file, output, "utf8"); console.error(`Results written to ${file}`); } else console.log(output); return 0; } export const __test = { build, filter, positional, pretty, sort, summary, }; if (process.argv[1] === fileURLToPath(import.meta.url)) { try { process.exit(main()); } catch (e) { console.error(`Error: ${e.message}`); process.exit(1); } }