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);
|
||||||
Executable
+54
@@ -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<string>({
|
||||||
|
message: "Test which solution?",
|
||||||
|
placeholder: "Type to search...",
|
||||||
|
maxItems: 12,
|
||||||
|
options: files.map((f) => {
|
||||||
|
// work layout: Difficulty/Category/<id>.<slug>.<ext>
|
||||||
|
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);
|
||||||
Reference in New Issue
Block a user