2026-08-26 15:25:35 -04:00
|
|
|
/**
|
|
|
|
|
* Shared work/ solution picker for the test and submit entries: fuzzy-select
|
2026-08-26 15:50:58 -04:00
|
|
|
* a solution file, then hand it to leetcode-cli.
|
|
|
|
|
*
|
2026-08-31 10:41:21 -04:00
|
|
|
* 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.
|
|
|
|
|
*
|
2026-08-26 15:50:58 -04:00
|
|
|
* 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.
|
2026-08-26 15:25:35 -04:00
|
|
|
*/
|
|
|
|
|
import { autocomplete, cancel, isCancel, log } from "@clack/prompts";
|
2026-08-26 15:50:58 -04:00
|
|
|
import { statSync } from "node:fs";
|
|
|
|
|
import { basename, join, relative } from "node:path";
|
|
|
|
|
import { problemDb } from "./db";
|
2026-08-31 10:41:21 -04:00
|
|
|
import { ROOT, WORK as WORK_DIR, parseWorkPath } from "./work";
|
2026-08-26 15:25:35 -04:00
|
|
|
|
2026-08-26 15:50:58 -04:00
|
|
|
/** 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-26 15:25:35 -04:00
|
|
|
/** Fuzzy-pick a solution under work/ and run `leetcode <command>` on it. */
|
|
|
|
|
export async function runOnSolution(command: "test" | "submit", message: string): Promise<never> {
|
2026-08-26 15:50:58 -04:00
|
|
|
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 });
|
|
|
|
|
});
|
2026-08-26 15:25:35 -04:00
|
|
|
|
|
|
|
|
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) => {
|
2026-08-31 10:41:21 -04:00
|
|
|
const parsed = parseWorkPath(f);
|
|
|
|
|
const stage = parsed?.bucket === 1 ? "first solve" : `${parsed?.bucket}-day review`;
|
2026-08-26 15:25:35 -04:00
|
|
|
return {
|
|
|
|
|
value: f,
|
2026-08-31 10:41:21 -04:00
|
|
|
label: basename(f),
|
|
|
|
|
hint: parsed ? `${stage} · ${parsed.difficulty} · ${parsed.category}` : f,
|
2026-08-26 15:25:35 -04:00
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
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"] },
|
|
|
|
|
);
|
2026-08-26 15:50:58 -04:00
|
|
|
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);
|
2026-08-26 15:25:35 -04:00
|
|
|
}
|