mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +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