/** * 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, and at most once a day folds in the signed-in * LeetCode profile's submission history (see db.ts), which survives a lost db * or a fresh clone. Files git reports as modified, added or * untracked come first, newest edit first — the problem being worked on is * the focused option, so Enter runs it without a search. The rest are ordered * 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, spinner } from "@clack/prompts"; import { statSync } from "node:fs"; import { basename, join, relative } from "node:path"; import { fetchSubmissions, leetcodeCredentials, 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; } /** * work/-relative paths of solutions git sees as modified, added or untracked, * mapped to their mtime. Empty outside a git checkout. */ function dirtySolutions(scanned: Set): Map { const git = Bun.spawnSync( ["git", "status", "--porcelain", "-z", "--untracked-files=all", "--", WORK_DIR], { cwd: ROOT }, ); const dirty = new Map(); if (!git.success) return dirty; const entries = git.stdout.toString().split("\0"); for (let i = 0; i < entries.length; i++) { const entry = entries[i]!; // renames/copies carry their source path as the next entry; skip it if (/[RC]/.test(entry.slice(0, 2))) i++; const rel = relative(WORK_DIR, join(ROOT, entry.slice(3))); // deletions and non-solution files are not in the scan if (scanned.has(rel)) dirty.set(rel, statSync(join(WORK_DIR, rel)).mtimeMs); } return dirty; } /** Fuzzy-pick a solution under work/ and run `leetcode ` on it. */ export async function runOnSolution( command: "test" | "submit", message: string, ): Promise { 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(), ); } // the profile's history: first and latest submission per work/ problem if (db.historyStale()) { const auth = leetcodeCredentials(); if (!auth) { log.warn("Not signed in to leetcode-cli — run `leetcode login` to restore history from your profile."); } else { const s = spinner(); s.start("Syncing submission history from your LeetCode profile"); try { const bySlug = new Map(); for (const f of scanned) { const parsed = parseWorkPath(f); if (parsed) bySlug.set(parsed.slug, parsed.lc); } const history = new Map(); for (const { slug, at } of await fetchSubmissions(auth)) { const id = bySlug.get(slug); if (id === undefined) continue; const seen = history.get(id); if (!seen) history.set(id, { first: at, last: at }); else if (at < seen.first) seen.first = at; else if (at > seen.last) seen.last = at; } s.stop(`Synced submission history for ${db.foldHistory(history)} solutions`); } catch (err) { s.error(`Submission history sync failed (${err}) — using cached timestamps`); } } } 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 dirty = dirtySolutions(new Set(scanned)); const files = scanned.sort((a, b) => { const ma = dirty.get(a); const mb = dirty.get(b); if (ma !== mb) { if (ma === undefined) return 1; if (mb === undefined) return -1; return mb - ma; } 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({ 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 ? `${dirty.has(f) ? "uncommitted · " : ""}${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); }