mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-16 23:16:26 +00:00
feat(scripts): add fuzzy picker and tester CLI utilities for LeetCode problems
This commit is contained in:
Executable
+96
@@ -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<Problem[]> {
|
||||
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<Problem>({
|
||||
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);
|
||||
Reference in New Issue
Block a user