From fce2c4f93d376c5d81b75fcf3daa81206855c799 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Thu, 20 Aug 2026 12:00:10 -0400 Subject: [PATCH] feat(scripts): add fuzzy picker and tester CLI utilities for LeetCode problems --- scripts/pick.ts | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ scripts/test.ts | 54 ++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100755 scripts/pick.ts create mode 100755 scripts/test.ts diff --git a/scripts/pick.ts b/scripts/pick.ts new file mode 100755 index 0000000..96673c8 --- /dev/null +++ b/scripts/pick.ts @@ -0,0 +1,96 @@ +#!/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); diff --git a/scripts/test.ts b/scripts/test.ts new file mode 100755 index 0000000..14b1732 --- /dev/null +++ b/scripts/test.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env bun +/** + * Fuzzy-select a solution file from work/ and run leetcode-cli tests on it. + */ +import { autocomplete, cancel, isCancel, log } from "@clack/prompts"; +import { join, relative } from "node:path"; + +const ROOT = join(import.meta.dir, ".."); +const WORK_DIR = join(ROOT, "work"); + +const files = [...new Bun.Glob("**/*.{js,ts,py,java,c,cpp,go,rs,rb,swift,kt,cs}").scanSync({ cwd: WORK_DIR })] + .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); + +if (files.length === 0) { + log.error(`No solution files found in ${relative(process.cwd(), WORK_DIR)}/`); + process.exit(1); +} + +const picked = await autocomplete({ + message: "Test which solution?", + placeholder: "Type to search...", + maxItems: 12, + options: files.map((f) => { + // work layout: Difficulty/Category/.. + const [difficulty, category, name] = f.split("/"); + return { + value: f, + label: name ?? f, + hint: category ? `${difficulty} ยท ${category}` : difficulty, + }; + }), + filter: (search, option) => { + const haystack = option.value.toLowerCase(); + return search + .toLowerCase() + .split(/\s+/) + .every((token) => haystack.includes(token)); + }, +}); + +if (isCancel(picked)) { + cancel("Nothing selected."); + process.exit(0); +} + +const proc = Bun.spawn( + [ + join(ROOT, "node_modules", ".bin", "leetcode"), + "test", + join(WORK_DIR, picked), + ], + { cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] }, +); +process.exit(await proc.exited);