#!/usr/bin/env bun /** * Fuzzy-pick a LeetCode problem and scaffold it via leetcode-cli. * * The problem index lives in the local SQLite cache (see db.ts), refreshed at * most once per 24h; campaign problem descriptions from README.md are * backfilled into the same db on the way. Problems that already have a * numbered file under work/ are excluded from the picker — re-picking them * would only clobber the existing scaffold. */ import { autocomplete, cancel, isCancel, spinner } from "@clack/prompts"; import { basename, join } from "node:path"; import { fetchDescription, problemDb, readmeSlugs, type Problem } from "./db"; const ROOT = join(import.meta.dir, "..", ".."); const WORK_DIR = join(ROOT, "work"); const db = problemDb(); // ── problem index (24h TTL) ────────────────────────────────────── if (db.indexStale()) { const s = spinner(); s.start("Fetching problem index from leetcode.com"); try { s.stop(`Indexed ${await db.refreshIndex()} problems`); } catch (err) { if (db.all().length === 0) { s.error(`Fetch failed: ${err}`); process.exit(1); } s.stop(`Index refresh failed (${err}) — using cached copy`); } } // ── description backfill for the campaign problems in README.md ── const missing = db.missingDescriptions(readmeSlugs(await Bun.file(join(ROOT, "README.md")).text())); if (missing.length > 0) { const s = spinner(); s.start(`Caching ${missing.length} problem descriptions`); let fetched = 0; let failed = 0; await Promise.all( Array.from({ length: 6 }, async () => { for (let slug = missing.shift(); slug; slug = missing.shift()) { try { const html = await fetchDescription(slug); if (html !== null) db.saveDescription(slug, html); s.message(`Caching problem descriptions (${++fetched})`); } catch { failed++; // left NULL — retried on the next run } } }), ); s.stop(failed ? `Cached ${fetched} descriptions (${failed} failed, will retry next run)` : `Cached ${fetched} problem descriptions`); } // ── exclude problems already picked or solved under work/ ──────── const done = new Set(); for (const rel of new Bun.Glob("**/*.*").scanSync({ cwd: WORK_DIR })) { const m = basename(rel).match(/^(\d+)\./); if (m) done.add(Number(m[1])); } const problems = db.all().filter((p) => !done.has(p.id)); db.close(); 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( // bun installs workspace-dep bins into this package's node_modules, not the root's [join(import.meta.dir, "node_modules", ".bin", "leetcode"), "pick", picked.slug], { cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] }, ); process.exit(await proc.exited);