#!/usr/bin/env bun /** * Fuzzy-pick a LeetCode problem and scaffold it via leetcode-cli. * Problem index is fetched from leetcode.com and cached for 24h in .cache/. */ import { autocomplete, cancel, isCancel, spinner } from "@clack/prompts"; import { mkdir } from "node:fs/promises"; import { join } from "node:path"; const ROOT = join(import.meta.dir, ".."); const CACHE_FILE = join(ROOT, ".cache", "leetcode-problems.json"); const CACHE_TTL_MS = 24 * 60 * 60 * 1000; interface Problem { id: number; title: string; slug: string; difficulty: "Easy" | "Medium" | "Hard"; paidOnly: boolean; } const DIFFICULTY = ["", "Easy", "Medium", "Hard"] as const; async function loadProblems(): Promise { const file = Bun.file(CACHE_FILE); if (await file.exists()) { const stale = Date.now() - file.lastModified > CACHE_TTL_MS; if (!stale) return file.json(); } const s = spinner(); s.start("Fetching problem index from leetcode.com"); const res = await fetch("https://leetcode.com/api/problems/all/", { headers: { "user-agent": "Mozilla/5.0" }, }); if (!res.ok) { s.error(`Fetch failed: ${res.status} ${res.statusText}`); process.exit(1); } const data = (await res.json()) as { stat_status_pairs: Array<{ stat: { frontend_question_id: number; question__title: string; question__title_slug: string; }; difficulty: { level: 1 | 2 | 3 }; paid_only: boolean; }>; }; const problems: Problem[] = data.stat_status_pairs .map((p) => ({ id: p.stat.frontend_question_id, title: p.stat.question__title, slug: p.stat.question__title_slug, difficulty: DIFFICULTY[p.difficulty.level] as Problem["difficulty"], paidOnly: p.paid_only, })) .sort((a, b) => a.id - b.id); s.stop(`Loaded ${problems.length} problems`); await mkdir(join(ROOT, ".cache"), { recursive: true }); await Bun.write(CACHE_FILE, JSON.stringify(problems)); return problems; } const problems = await loadProblems(); const picked = await autocomplete({ message: "Pick a problem", placeholder: "Type to search by number or title...", maxItems: 12, options: problems.map((p) => ({ value: p, label: `${p.id}. ${p.title}`, hint: p.paidOnly ? `${p.difficulty} 🔒 premium` : p.difficulty, })), filter: (search, option) => { const haystack = option.label!.toLowerCase(); return search .toLowerCase() .split(/\s+/) .every((token) => haystack.includes(token)); }, }); if (isCancel(picked)) { cancel("Nothing picked."); process.exit(0); } const proc = Bun.spawn( [join(ROOT, "node_modules", ".bin", "leetcode"), "pick", picked.slug], { cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] }, ); process.exit(await proc.exited);