ace-interview-prep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/commands/add.js +92 -0
  4. package/dist/commands/feedback.js +133 -0
  5. package/dist/commands/generate.js +224 -0
  6. package/dist/commands/init.js +100 -0
  7. package/dist/commands/list.js +107 -0
  8. package/dist/commands/reset.js +68 -0
  9. package/dist/commands/score.js +70 -0
  10. package/dist/commands/setup.js +84 -0
  11. package/dist/commands/test.js +85 -0
  12. package/dist/index.js +72 -0
  13. package/dist/lib/categories.js +103 -0
  14. package/dist/lib/config.js +61 -0
  15. package/dist/lib/llm.js +134 -0
  16. package/dist/lib/paths.js +38 -0
  17. package/dist/lib/scaffold.js +110 -0
  18. package/dist/lib/scorecard.js +116 -0
  19. package/dist/prompts/code-review.md +59 -0
  20. package/dist/prompts/design-review.md +67 -0
  21. package/dist/prompts/question-brainstorm.md +31 -0
  22. package/dist/prompts/question-generate.md +65 -0
  23. package/dist/templates/design/notes.md.hbs +27 -0
  24. package/dist/templates/js-ts/solution.test.ts.hbs +11 -0
  25. package/dist/templates/js-ts/solution.ts.hbs +11 -0
  26. package/dist/templates/leetcode-algo/solution.test.ts.hbs +11 -0
  27. package/dist/templates/leetcode-algo/solution.ts.hbs +11 -0
  28. package/dist/templates/leetcode-ds/solution.test.ts.hbs +11 -0
  29. package/dist/templates/leetcode-ds/solution.ts.hbs +11 -0
  30. package/dist/templates/react-apps/App.test.tsx.hbs +16 -0
  31. package/dist/templates/react-apps/App.tsx.hbs +16 -0
  32. package/dist/templates/readme.md.hbs +9 -0
  33. package/dist/templates/web-components/component.test.ts.hbs +11 -0
  34. package/dist/templates/web-components/component.ts.hbs +22 -0
  35. package/dist/templates/web-components/index.html.hbs +12 -0
  36. package/package.json +72 -0
  37. package/questions/design-be/url-shortener/README.md +23 -0
  38. package/questions/design-be/url-shortener/notes.md +27 -0
  39. package/questions/design-be/url-shortener/scorecard.json +1 -0
  40. package/questions/design-fe/news-feed/README.md +22 -0
  41. package/questions/design-fe/news-feed/notes.md +27 -0
  42. package/questions/design-fe/news-feed/scorecard.json +1 -0
  43. package/questions/design-full/google-docs/README.md +22 -0
  44. package/questions/design-full/google-docs/notes.md +27 -0
  45. package/questions/design-full/google-docs/scorecard.json +1 -0
  46. package/questions/js-ts/debounce/README.md +86 -0
  47. package/questions/js-ts/debounce/scorecard.json +9 -0
  48. package/questions/js-ts/debounce/solution.test.ts +128 -0
  49. package/questions/js-ts/debounce/solution.ts +4 -0
  50. package/questions/leetcode-algo/two-sum/README.md +58 -0
  51. package/questions/leetcode-algo/two-sum/scorecard.json +1 -0
  52. package/questions/leetcode-algo/two-sum/solution.test.ts +55 -0
  53. package/questions/leetcode-algo/two-sum/solution.ts +4 -0
  54. package/questions/leetcode-ds/lru-cache/README.md +70 -0
  55. package/questions/leetcode-ds/lru-cache/scorecard.json +1 -0
  56. package/questions/leetcode-ds/lru-cache/solution.test.ts +82 -0
  57. package/questions/leetcode-ds/lru-cache/solution.ts +14 -0
  58. package/questions/react-apps/todo-app/App.test.tsx +130 -0
  59. package/questions/react-apps/todo-app/App.tsx +10 -0
  60. package/questions/react-apps/todo-app/README.md +23 -0
  61. package/questions/react-apps/todo-app/scorecard.json +9 -0
  62. package/questions/web-components/star-rating/README.md +45 -0
  63. package/questions/web-components/star-rating/component.test.ts +64 -0
  64. package/questions/web-components/star-rating/component.ts +28 -0
  65. package/questions/web-components/star-rating/index.html +14 -0
  66. package/questions/web-components/star-rating/scorecard.json +9 -0
@@ -0,0 +1,107 @@
1
+ import chalk from "chalk";
2
+ import Table from "cli-table3";
3
+ import { CATEGORIES } from "../lib/categories.js";
4
+ import { getAllQuestions } from "../lib/scorecard.js";
5
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
6
+ function parseArgs(args) {
7
+ const result = {};
8
+ for (let i = 0; i < args.length; i++) {
9
+ const arg = args[i];
10
+ if (arg.startsWith("--")) {
11
+ const key = arg.slice(2);
12
+ const next = args[i + 1];
13
+ if (next && !next.startsWith("--")) {
14
+ result[key] = next;
15
+ i++;
16
+ } else {
17
+ result[key] = "true";
18
+ }
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ const STATUS_COLORS = {
24
+ untouched: chalk.gray,
25
+ "in-progress": chalk.yellow,
26
+ attempted: chalk.red,
27
+ solved: chalk.green
28
+ };
29
+ const DIFFICULTY_COLORS = {
30
+ easy: chalk.green,
31
+ medium: chalk.yellow,
32
+ hard: chalk.red
33
+ };
34
+ async function run(args) {
35
+ const root = resolveWorkspaceRoot();
36
+ if (!isWorkspaceInitialized(root)) {
37
+ console.error(chalk.red("\nError: Workspace not initialized."));
38
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
39
+ process.exit(1);
40
+ }
41
+ const parsed = parseArgs(args);
42
+ const filterCategory = parsed.category;
43
+ const filterStatus = parsed.status;
44
+ const filterDifficulty = parsed.difficulty;
45
+ let questions = getAllQuestions();
46
+ if (filterCategory) {
47
+ questions = questions.filter((q) => q.category === filterCategory);
48
+ }
49
+ if (filterStatus) {
50
+ questions = questions.filter((q) => q.scorecard.status === filterStatus);
51
+ }
52
+ if (filterDifficulty) {
53
+ questions = questions.filter((q) => q.scorecard.difficulty === filterDifficulty);
54
+ }
55
+ if (questions.length === 0) {
56
+ console.log(chalk.yellow("\nNo questions found."));
57
+ console.log(chalk.dim("Use `npm run ace generate` or `npm run ace add` to create one."));
58
+ return;
59
+ }
60
+ const table = new Table({
61
+ head: [
62
+ chalk.bold("Category"),
63
+ chalk.bold("Question"),
64
+ chalk.bold("Difficulty"),
65
+ chalk.bold("Status"),
66
+ chalk.bold("~Time")
67
+ ],
68
+ style: { head: [], border: [] }
69
+ });
70
+ questions.sort((a, b) => {
71
+ if (a.category !== b.category) return a.category.localeCompare(b.category);
72
+ return a.slug.localeCompare(b.slug);
73
+ });
74
+ for (const q of questions) {
75
+ const config = CATEGORIES[q.category];
76
+ const sc = q.scorecard;
77
+ const diffColor = DIFFICULTY_COLORS[sc.difficulty] || chalk.white;
78
+ const statusColor = STATUS_COLORS[sc.status] || chalk.white;
79
+ table.push([
80
+ config?.shortName || q.category,
81
+ q.slug,
82
+ diffColor(sc.difficulty),
83
+ statusColor(sc.status),
84
+ `~${sc.suggestedTime}m`
85
+ ]);
86
+ }
87
+ console.log(`
88
+ ${chalk.bold.cyan("ace")} \u2014 Question Dashboard
89
+ `);
90
+ console.log(table.toString());
91
+ console.log(chalk.dim(`
92
+ ${questions.length} question(s) total`));
93
+ const solved = questions.filter((q) => q.scorecard.status === "solved").length;
94
+ const attempted = questions.filter((q) => q.scorecard.status === "attempted").length;
95
+ const inProgress = questions.filter((q) => q.scorecard.status === "in-progress").length;
96
+ if (solved > 0 || attempted > 0 || inProgress > 0) {
97
+ console.log(
98
+ chalk.dim(
99
+ ` ${chalk.green(String(solved))} solved \xB7 ${chalk.yellow(String(inProgress))} in-progress \xB7 ${chalk.red(String(attempted))} attempted`
100
+ )
101
+ );
102
+ }
103
+ console.log(chalk.dim("\n Filters: --category <slug> | --status <status> | --difficulty <level>\n"));
104
+ }
105
+ export {
106
+ run
107
+ };
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { findQuestion, readScorecard, resetScorecard, startNewAttempt, writeScorecard } from "../lib/scorecard.js";
6
+ import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
7
+ import { getStubContent } from "../lib/scaffold.js";
8
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
9
+ async function run(args) {
10
+ const root = resolveWorkspaceRoot();
11
+ if (!isWorkspaceInitialized(root)) {
12
+ console.error(chalk.red("\nError: Workspace not initialized."));
13
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
14
+ process.exit(1);
15
+ }
16
+ const slug = args.find((a) => !a.startsWith("--"));
17
+ if (!slug) {
18
+ console.error(chalk.red("Missing question slug."));
19
+ console.error(chalk.dim("Usage: npm run ace reset <slug>"));
20
+ return;
21
+ }
22
+ const question = findQuestion(slug);
23
+ if (!question) {
24
+ console.error(chalk.red(`Question not found: ${slug}`));
25
+ return;
26
+ }
27
+ const { confirm } = await prompts({
28
+ type: "confirm",
29
+ name: "confirm",
30
+ message: `Reset "${slug}" to unanswered? This will clear your solution.`,
31
+ initial: false
32
+ });
33
+ if (!confirm) {
34
+ console.log(chalk.yellow("Cancelled."));
35
+ return;
36
+ }
37
+ const config = CATEGORIES[question.category];
38
+ const isDesign = isDesignCategory(question.category);
39
+ if (isDesign) {
40
+ const notesPath = path.join(question.dir, "notes.md");
41
+ const stubContent = getStubContent(question.category, "notes.md");
42
+ if (stubContent) {
43
+ fs.writeFileSync(notesPath, stubContent);
44
+ }
45
+ } else {
46
+ for (const file of config.solutionFiles) {
47
+ if (file === "index.html") continue;
48
+ const filePath = path.join(question.dir, file);
49
+ const stubContent = getStubContent(question.category, file);
50
+ if (stubContent) {
51
+ fs.writeFileSync(filePath, stubContent);
52
+ }
53
+ }
54
+ }
55
+ const scorecard = readScorecard(question.category, slug);
56
+ if (scorecard) {
57
+ resetScorecard(scorecard);
58
+ startNewAttempt(scorecard);
59
+ scorecard.status = "untouched";
60
+ writeScorecard(question.category, slug, scorecard);
61
+ }
62
+ console.log(chalk.green(`
63
+ Reset: questions/${question.category}/${slug}/`));
64
+ console.log(chalk.dim("Solution files restored to stubs. Scorecard updated."));
65
+ }
66
+ export {
67
+ run
68
+ };
@@ -0,0 +1,70 @@
1
+ import chalk from "chalk";
2
+ import { findQuestion, readScorecard } from "../lib/scorecard.js";
3
+ import { CATEGORIES } from "../lib/categories.js";
4
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
5
+ async function run(args) {
6
+ const root = resolveWorkspaceRoot();
7
+ if (!isWorkspaceInitialized(root)) {
8
+ console.error(chalk.red("\nError: Workspace not initialized."));
9
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
10
+ process.exit(1);
11
+ }
12
+ const slug = args.find((a) => !a.startsWith("--"));
13
+ if (!slug) {
14
+ console.error(chalk.red("Missing question slug."));
15
+ console.error(chalk.dim("Usage: npm run ace score <slug>"));
16
+ return;
17
+ }
18
+ const question = findQuestion(slug);
19
+ if (!question) {
20
+ console.error(chalk.red(`Question not found: ${slug}`));
21
+ return;
22
+ }
23
+ const scorecard = readScorecard(question.category, slug);
24
+ if (!scorecard) {
25
+ console.error(chalk.red("No scorecard found for this question."));
26
+ return;
27
+ }
28
+ const config = CATEGORIES[question.category];
29
+ console.log(`
30
+ ${chalk.bold.cyan("Scorecard:")} ${chalk.bold(scorecard.title || slug)}`);
31
+ console.log(chalk.dim("\u2500".repeat(60)));
32
+ console.log(` ${chalk.bold("Category:")} ${config?.name || question.category}`);
33
+ console.log(` ${chalk.bold("Difficulty:")} ${scorecard.difficulty}`);
34
+ console.log(` ${chalk.bold("Suggested:")} ~${scorecard.suggestedTime} minutes`);
35
+ const statusColors = {
36
+ untouched: chalk.gray,
37
+ "in-progress": chalk.yellow,
38
+ attempted: chalk.red,
39
+ solved: chalk.green
40
+ };
41
+ const statusColor = statusColors[scorecard.status] || chalk.white;
42
+ console.log(` ${chalk.bold("Status:")} ${statusColor(scorecard.status)}`);
43
+ if (scorecard.attempts.length > 0) {
44
+ console.log(`
45
+ ${chalk.bold(" Attempts:")}`);
46
+ for (const attempt of scorecard.attempts) {
47
+ const testInfo = attempt.testsTotal > 0 ? `${attempt.testsPassed}/${attempt.testsTotal} tests` : "no tests run";
48
+ const scoreInfo = attempt.llmScore !== null ? ` \xB7 LLM: ${attempt.llmScore}/5` : "";
49
+ const color = attempt.testsPassed === attempt.testsTotal && attempt.testsTotal > 0 ? chalk.green : chalk.yellow;
50
+ console.log(` #${attempt.attempt}: ${color(testInfo)}${scoreInfo}`);
51
+ }
52
+ } else {
53
+ console.log(chalk.dim("\n No attempts yet."));
54
+ }
55
+ if (scorecard.llmFeedback) {
56
+ console.log(`
57
+ ${chalk.bold(" Last LLM Feedback:")}`);
58
+ const lines = scorecard.llmFeedback.split("\n").slice(0, 15);
59
+ for (const line of lines) {
60
+ console.log(` ${chalk.dim(line)}`);
61
+ }
62
+ if (scorecard.llmFeedback.split("\n").length > 15) {
63
+ console.log(chalk.dim(" ... (run `ace feedback` for full review)"));
64
+ }
65
+ }
66
+ console.log(chalk.dim("\n" + "\u2500".repeat(60)));
67
+ }
68
+ export {
69
+ run
70
+ };
@@ -0,0 +1,84 @@
1
+ import prompts from "prompts";
2
+ import chalk from "chalk";
3
+ import { saveGlobalAceConfig, maskApiKey, loadAceConfig } from "../lib/config.js";
4
+ function parseArgs(args) {
5
+ const result = {};
6
+ for (let i = 0; i < args.length; i++) {
7
+ const arg = args[i];
8
+ if (arg.startsWith("--")) {
9
+ const key = arg.slice(2);
10
+ const next = args[i + 1];
11
+ if (next && !next.startsWith("--")) {
12
+ result[key] = next;
13
+ i++;
14
+ } else {
15
+ result[key] = "true";
16
+ }
17
+ }
18
+ }
19
+ return result;
20
+ }
21
+ async function run(args) {
22
+ const parsed = parseArgs(args);
23
+ console.log(chalk.cyan("\n--- Setup API Keys ---"));
24
+ console.log(chalk.dim("API keys will be stored in ~/.ace/config.json\n"));
25
+ let openaiKey = parsed["openai-key"];
26
+ let anthropicKey = parsed["anthropic-key"];
27
+ const existing = loadAceConfig();
28
+ if (!openaiKey && !anthropicKey) {
29
+ const { openai } = await prompts({
30
+ type: "text",
31
+ name: "openai",
32
+ message: "OpenAI API Key (leave blank to skip):",
33
+ initial: existing.OPENAI_API_KEY ? maskApiKey(existing.OPENAI_API_KEY) : ""
34
+ });
35
+ const { anthropic } = await prompts({
36
+ type: "text",
37
+ name: "anthropic",
38
+ message: "Anthropic API Key (leave blank to skip):",
39
+ initial: existing.ANTHROPIC_API_KEY ? maskApiKey(existing.ANTHROPIC_API_KEY) : ""
40
+ });
41
+ openaiKey = openai;
42
+ anthropicKey = anthropic;
43
+ }
44
+ if (openaiKey && openaiKey.startsWith("...")) {
45
+ openaiKey = void 0;
46
+ }
47
+ if (anthropicKey && anthropicKey.startsWith("...")) {
48
+ anthropicKey = void 0;
49
+ }
50
+ const hasNewOpenAI = openaiKey && openaiKey.trim().length > 0;
51
+ const hasNewAnthropic = anthropicKey && anthropicKey.trim().length > 0;
52
+ const hasExistingOpenAI = existing.OPENAI_API_KEY && !hasNewOpenAI;
53
+ const hasExistingAnthropic = existing.ANTHROPIC_API_KEY && !hasNewAnthropic;
54
+ if (!hasNewOpenAI && !hasNewAnthropic && !hasExistingOpenAI && !hasExistingAnthropic) {
55
+ console.error(chalk.red("\nError: At least one API key is required."));
56
+ console.error(chalk.dim("Provide --openai-key or --anthropic-key, or enter keys when prompted."));
57
+ process.exit(1);
58
+ }
59
+ const toSave = {};
60
+ if (hasNewOpenAI) {
61
+ toSave.OPENAI_API_KEY = openaiKey.trim();
62
+ }
63
+ if (hasNewAnthropic) {
64
+ toSave.ANTHROPIC_API_KEY = anthropicKey.trim();
65
+ }
66
+ if (Object.keys(toSave).length > 0) {
67
+ saveGlobalAceConfig(toSave);
68
+ console.log(chalk.green("\n\u2713 API keys saved to ~/.ace/config.json"));
69
+ } else {
70
+ console.log(chalk.yellow("\nNo new keys provided. Existing configuration unchanged."));
71
+ }
72
+ const final = loadAceConfig();
73
+ console.log(chalk.dim("\nConfigured providers:"));
74
+ if (final.OPENAI_API_KEY) {
75
+ console.log(chalk.dim(` \u2022 OpenAI: ${maskApiKey(final.OPENAI_API_KEY)}`));
76
+ }
77
+ if (final.ANTHROPIC_API_KEY) {
78
+ console.log(chalk.dim(` \u2022 Anthropic: ${maskApiKey(final.ANTHROPIC_API_KEY)}`));
79
+ }
80
+ console.log();
81
+ }
82
+ export {
83
+ run
84
+ };
@@ -0,0 +1,85 @@
1
+ import { execSync } from "node:child_process";
2
+ import chalk from "chalk";
3
+ import { findQuestion, readScorecard, updateTestResults, writeScorecard } from "../lib/scorecard.js";
4
+ import { isDesignCategory } from "../lib/categories.js";
5
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
6
+ function parseArgs(args) {
7
+ let slug;
8
+ let watch = false;
9
+ for (const arg of args) {
10
+ if (arg === "--watch") {
11
+ watch = true;
12
+ } else if (!arg.startsWith("--")) {
13
+ slug = arg;
14
+ }
15
+ }
16
+ return { slug, watch };
17
+ }
18
+ function parseTestOutput(output) {
19
+ const passMatch = output.match(/(\d+)\s+passed/);
20
+ const failMatch = output.match(/(\d+)\s+failed/);
21
+ const totalMatch = output.match(/Tests\s+(\d+)/);
22
+ const passed = passMatch ? parseInt(passMatch[1], 10) : 0;
23
+ const failed = failMatch ? parseInt(failMatch[1], 10) : 0;
24
+ const total = totalMatch ? parseInt(totalMatch[1], 10) : passed + failed;
25
+ return { total, passed };
26
+ }
27
+ async function run(args) {
28
+ const projectRoot = resolveWorkspaceRoot();
29
+ if (!isWorkspaceInitialized(projectRoot)) {
30
+ console.error(chalk.red("\nError: Workspace not initialized."));
31
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
32
+ process.exit(1);
33
+ }
34
+ const { slug, watch } = parseArgs(args);
35
+ if (!slug) {
36
+ console.log(chalk.cyan("\nRunning all tests...\n"));
37
+ try {
38
+ const cmd = watch ? "npx vitest" : "npx vitest run";
39
+ execSync(cmd, { cwd: projectRoot, stdio: "inherit" });
40
+ } catch {
41
+ }
42
+ return;
43
+ }
44
+ const question = findQuestion(slug);
45
+ if (!question) {
46
+ console.error(chalk.red(`Question not found: ${slug}`));
47
+ console.error(chalk.dim("Run `npm run ace list` to see all questions."));
48
+ return;
49
+ }
50
+ if (isDesignCategory(question.category)) {
51
+ console.log(chalk.yellow(`"${slug}" is a system design question \u2014 no tests to run.`));
52
+ console.log(chalk.dim("Use `npm run ace feedback " + slug + "` for LLM review."));
53
+ return;
54
+ }
55
+ console.log(chalk.cyan(`
56
+ Running tests for: ${slug}
57
+ `));
58
+ let output = "";
59
+ try {
60
+ const cmd = watch ? `npx vitest ${question.dir}` : `npx vitest run ${question.dir}`;
61
+ output = execSync(cmd, { cwd: projectRoot, encoding: "utf-8", stdio: ["inherit", "pipe", "pipe"] });
62
+ console.log(output);
63
+ } catch (err) {
64
+ if (err && typeof err === "object" && "stdout" in err) {
65
+ output = err.stdout || "";
66
+ console.log(output);
67
+ }
68
+ }
69
+ if (!watch) {
70
+ const scorecard = readScorecard(question.category, slug);
71
+ if (scorecard) {
72
+ const { total, passed } = parseTestOutput(output);
73
+ updateTestResults(scorecard, total, passed);
74
+ writeScorecard(question.category, slug, scorecard);
75
+ if (total > 0) {
76
+ const color = passed === total ? chalk.green : chalk.red;
77
+ console.log(color(`
78
+ Scorecard updated: ${passed}/${total} tests passed`));
79
+ }
80
+ }
81
+ }
82
+ }
83
+ export {
84
+ run
85
+ };
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ const [, , command, ...args] = process.argv;
4
+ const COMMANDS = {
5
+ setup: () => import("./commands/setup.js"),
6
+ init: () => import("./commands/init.js"),
7
+ generate: () => import("./commands/generate.js"),
8
+ add: () => import("./commands/add.js"),
9
+ list: () => import("./commands/list.js"),
10
+ test: () => import("./commands/test.js"),
11
+ feedback: () => import("./commands/feedback.js"),
12
+ reset: () => import("./commands/reset.js"),
13
+ score: () => import("./commands/score.js")
14
+ };
15
+ function printHelp() {
16
+ console.log(`
17
+ ${chalk.bold.cyan("ace")} \u2014 Frontend Interview Prep CLI
18
+
19
+ ${chalk.bold("Setup Commands:")}
20
+
21
+ ${chalk.green("setup")} Configure API keys (stored in ~/.ace)
22
+ ${chalk.green("init")} Initialize workspace with questions/ and test config
23
+
24
+ ${chalk.bold("Question Commands:")}
25
+
26
+ ${chalk.green("generate")} Generate a question using LLM (--brainstorm for interactive mode)
27
+ ${chalk.green("add")} Manually add a question with interactive prompts
28
+ ${chalk.green("list")} List all questions with status and filters
29
+ ${chalk.green("test")} Run tests for a question (or all questions)
30
+ ${chalk.green("feedback")} Get LLM code review or design review
31
+ ${chalk.green("reset")} Reset a question to unanswered state
32
+ ${chalk.green("score")} View scorecard for a question
33
+
34
+ ${chalk.bold("Examples:")}
35
+
36
+ ace setup
37
+ ace init
38
+ ace generate --topic "debounce" --category js-ts --difficulty medium
39
+ ace generate --brainstorm
40
+ ace list
41
+ ace list --category js-ts --status solved
42
+ ace test debounce
43
+ ace test --watch
44
+ ace feedback debounce
45
+ ace reset debounce
46
+ ace score debounce
47
+ `);
48
+ }
49
+ async function main() {
50
+ if (!command || command === "help" || command === "--help" || command === "-h") {
51
+ printHelp();
52
+ process.exit(0);
53
+ }
54
+ const loader = COMMANDS[command];
55
+ if (!loader) {
56
+ console.error(chalk.red(`Unknown command: ${command}`));
57
+ console.error(`Run ${chalk.cyan("npm run ace help")} for usage.`);
58
+ process.exit(1);
59
+ }
60
+ try {
61
+ const mod = await loader();
62
+ await mod.run(args);
63
+ } catch (err) {
64
+ if (err instanceof Error) {
65
+ console.error(chalk.red(`Error: ${err.message}`));
66
+ } else {
67
+ console.error(chalk.red("An unexpected error occurred"));
68
+ }
69
+ process.exit(1);
70
+ }
71
+ }
72
+ main();
@@ -0,0 +1,103 @@
1
+ const CATEGORIES = {
2
+ "js-ts": {
3
+ slug: "js-ts",
4
+ name: "JS/TS Puzzles",
5
+ shortName: "JS/TS",
6
+ type: "coding",
7
+ suggestedTimes: { easy: 15, medium: 30, hard: 45 },
8
+ solutionFiles: ["solution.ts"],
9
+ testFiles: ["solution.test.ts"],
10
+ templateDir: "js-ts"
11
+ },
12
+ "web-components": {
13
+ slug: "web-components",
14
+ name: "Web Components",
15
+ shortName: "WebComp",
16
+ type: "coding",
17
+ suggestedTimes: { easy: 20, medium: 35, hard: 50 },
18
+ solutionFiles: ["component.ts", "index.html"],
19
+ testFiles: ["component.test.ts"],
20
+ templateDir: "web-components"
21
+ },
22
+ "react-apps": {
23
+ slug: "react-apps",
24
+ name: "React Web Apps",
25
+ shortName: "React",
26
+ type: "coding",
27
+ suggestedTimes: { easy: 25, medium: 45, hard: 60 },
28
+ solutionFiles: ["App.tsx"],
29
+ testFiles: ["App.test.tsx"],
30
+ templateDir: "react-apps"
31
+ },
32
+ "leetcode-ds": {
33
+ slug: "leetcode-ds",
34
+ name: "LeetCode Data Structures",
35
+ shortName: "LC-DS",
36
+ type: "coding",
37
+ suggestedTimes: { easy: 15, medium: 30, hard: 45 },
38
+ solutionFiles: ["solution.ts"],
39
+ testFiles: ["solution.test.ts"],
40
+ templateDir: "leetcode-ds"
41
+ },
42
+ "leetcode-algo": {
43
+ slug: "leetcode-algo",
44
+ name: "LeetCode Algorithms",
45
+ shortName: "LC-Algo",
46
+ type: "coding",
47
+ suggestedTimes: { easy: 15, medium: 30, hard: 45 },
48
+ solutionFiles: ["solution.ts"],
49
+ testFiles: ["solution.test.ts"],
50
+ templateDir: "leetcode-algo"
51
+ },
52
+ "design-fe": {
53
+ slug: "design-fe",
54
+ name: "System Design \u2014 Frontend",
55
+ shortName: "Design-FE",
56
+ type: "design",
57
+ suggestedTimes: { easy: 25, medium: 40, hard: 55 },
58
+ solutionFiles: ["notes.md"],
59
+ testFiles: [],
60
+ templateDir: "design"
61
+ },
62
+ "design-be": {
63
+ slug: "design-be",
64
+ name: "System Design \u2014 Backend",
65
+ shortName: "Design-BE",
66
+ type: "design",
67
+ suggestedTimes: { easy: 25, medium: 40, hard: 55 },
68
+ solutionFiles: ["notes.md"],
69
+ testFiles: [],
70
+ templateDir: "design"
71
+ },
72
+ "design-full": {
73
+ slug: "design-full",
74
+ name: "System Design \u2014 Full Stack",
75
+ shortName: "Design-Full",
76
+ type: "design",
77
+ suggestedTimes: { easy: 30, medium: 45, hard: 60 },
78
+ solutionFiles: ["notes.md"],
79
+ testFiles: [],
80
+ templateDir: "design"
81
+ }
82
+ };
83
+ const CATEGORY_SLUGS = Object.keys(CATEGORIES);
84
+ function getCategoryConfig(slug) {
85
+ return CATEGORIES[slug];
86
+ }
87
+ function getSuggestedTime(slug, difficulty) {
88
+ return CATEGORIES[slug].suggestedTimes[difficulty];
89
+ }
90
+ function isDesignCategory(slug) {
91
+ return CATEGORIES[slug].type === "design";
92
+ }
93
+ function slugify(text) {
94
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
95
+ }
96
+ export {
97
+ CATEGORIES,
98
+ CATEGORY_SLUGS,
99
+ getCategoryConfig,
100
+ getSuggestedTime,
101
+ isDesignCategory,
102
+ slugify
103
+ };
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import dotenv from "dotenv";
4
+ import { getGlobalAceDir } from "./paths.js";
5
+ function loadAceConfig() {
6
+ const config = {};
7
+ const globalAceDir = getGlobalAceDir();
8
+ const configPath = path.join(globalAceDir, "config.json");
9
+ if (fs.existsSync(configPath)) {
10
+ try {
11
+ const content = fs.readFileSync(configPath, "utf-8");
12
+ const parsed = JSON.parse(content);
13
+ Object.assign(config, parsed);
14
+ } catch (err) {
15
+ }
16
+ }
17
+ const envPath = path.join(globalAceDir, ".env");
18
+ if (fs.existsSync(envPath)) {
19
+ const envConfig = dotenv.parse(fs.readFileSync(envPath));
20
+ for (const [key, value] of Object.entries(envConfig)) {
21
+ if (!config[key]) {
22
+ config[key] = value;
23
+ }
24
+ }
25
+ }
26
+ if (!config.OPENAI_API_KEY && process.env.OPENAI_API_KEY) {
27
+ config.OPENAI_API_KEY = process.env.OPENAI_API_KEY;
28
+ }
29
+ if (!config.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY) {
30
+ config.ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
31
+ }
32
+ return config;
33
+ }
34
+ function saveGlobalAceConfig(partial) {
35
+ const globalAceDir = getGlobalAceDir();
36
+ if (!fs.existsSync(globalAceDir)) {
37
+ fs.mkdirSync(globalAceDir, { recursive: true, mode: 448 });
38
+ }
39
+ const configPath = path.join(globalAceDir, "config.json");
40
+ let existing = {};
41
+ if (fs.existsSync(configPath)) {
42
+ try {
43
+ existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
44
+ } catch {
45
+ }
46
+ }
47
+ const merged = { ...existing, ...partial };
48
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", {
49
+ encoding: "utf-8",
50
+ mode: 384
51
+ });
52
+ }
53
+ function maskApiKey(key) {
54
+ if (!key || key.length < 8) return "***";
55
+ return "..." + key.slice(-4);
56
+ }
57
+ export {
58
+ loadAceConfig,
59
+ maskApiKey,
60
+ saveGlobalAceConfig
61
+ };