Files
leetcode/apps/cli/solutions.ts
T

116 lines
4.1 KiB
TypeScript
Raw Normal View History

/**
* Shared work/ solution picker for the test and submit entries: fuzzy-select
* a solution file, then hand it to leetcode-cli.
*
* Every bucket is listed — a 3-day or 7-day re-solve is tested and submitted
* exactly like a first solve — and the hint says which one a file belongs to.
* The db's per-solution timestamps are keyed by LC number, so the buckets of
* one problem share them.
*
* Every run backfills the db's solutions table with created_at (from the
* file's birthtime, mtime when the filesystem has none) so ordering works for
* files that predate the db. Ordering is per command — test lists the most
* recently tested first (never-tested after, by name); submit lists the most
* recently created first. Nothing is ever hidden. Exit 0 of leetcode-cli
* stamps last_tested / last_submitted.
*
* Library: side-effect-free on import — nothing scans the filesystem until
* runOnSolution() is called.
*/
import { autocomplete, cancel, isCancel, log } from "@clack/prompts";
import { statSync } from "node:fs";
import { basename, join, relative } from "node:path";
import { problemDb } from "./db";
import { ROOT, WORK as WORK_DIR, parseWorkPath } from "./work";
/** Leading LC number of a work/ file, or undefined for unnumbered names. */
function lcNumber(rel: string): number | undefined {
const m = basename(rel).match(/^(\d+)\./);
return m ? Number(m[1]) : undefined;
}
/** Fuzzy-pick a solution under work/ and run `leetcode <command>` on it. */
export async function runOnSolution(command: "test" | "submit", message: string): Promise<never> {
const db = problemDb();
const scanned = [...new Bun.Glob("**/*.{js,ts,py,java,c,cpp,go,rs,rb,swift,kt,cs}").scanSync({ cwd: WORK_DIR })];
// register creation times so files older than the db still sort correctly
for (const f of scanned) {
const num = lcNumber(f);
if (num === undefined) continue;
const stat = statSync(join(WORK_DIR, f));
db.ensureSolution(num, new Date(stat.birthtimeMs || stat.mtimeMs).toISOString());
}
const activity = db.solutionActivity();
// test → most recently tested first; submit → most recently created first
const sortKey = (f: string) => {
const num = lcNumber(f);
const row = num === undefined ? undefined : activity.get(num);
return (command === "test" ? row?.lastTested : row?.createdAt) ?? undefined;
};
const files = scanned
.sort((a, b) => {
const ka = sortKey(a);
const kb = sortKey(b);
if (ka !== kb) {
if (ka === undefined) return 1;
if (kb === undefined) return -1;
return kb.localeCompare(ka); // ISO timestamps: lexicographic = chronological
}
return 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,
placeholder: "Type to search...",
maxItems: 12,
options: files.map((f) => {
const parsed = parseWorkPath(f);
const stage = parsed?.bucket === 1 ? "first solve" : `${parsed?.bucket}-day review`;
return {
value: f,
label: basename(f),
hint: parsed ? `${stage} · ${parsed.difficulty} · ${parsed.category}` : f,
};
}),
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(
[
// bun installs workspace-dep bins into this package's node_modules, not the root's
join(import.meta.dir, "node_modules", ".bin", "leetcode"),
command,
join(WORK_DIR, picked),
],
{ cwd: ROOT, stdio: ["inherit", "inherit", "inherit"] },
);
const code = await proc.exited;
const num = lcNumber(picked);
if (code === 0 && num !== undefined) {
if (command === "test") db.markTested(num);
else db.markSubmitted(num);
}
db.close();
process.exit(code);
}