mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +00:00
98 lines
2.8 KiB
TypeScript
Executable File
98 lines
2.8 KiB
TypeScript
Executable File
#!/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(
|
|
// 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);
|