ace-interview-prep 0.1.2 → 0.1.3

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.
package/README.md CHANGED
@@ -50,7 +50,10 @@ npm install vitest happy-dom @testing-library/jest-dom
50
50
  ### 3. Practice
51
51
 
52
52
  ```bash
53
- # Generate a question via LLM
53
+ # Generate a question interactively (prompts for category, difficulty, topic)
54
+ ace generate
55
+
56
+ # Or pass flags to skip prompts
54
57
  ace generate --topic "debounce" --category js-ts --difficulty medium
55
58
 
56
59
  # Interactive brainstorm mode
@@ -61,43 +64,59 @@ ace add
61
64
 
62
65
  # List all questions
63
66
  ace list
67
+ ```
68
+
69
+ ### 4. Test, Review, Track
64
70
 
71
+ All commands below work in three modes:
72
+ - **Interactive** — run with no arguments to pick from a selectable list
73
+ - **Direct** — pass a slug to target a specific question
74
+ - **All** — pass `--all` to operate on every question
75
+
76
+ ```bash
65
77
  # Run tests
66
- ace test debounce
67
- ace test # run all
68
- ace test --watch # watch mode
78
+ ace test # pick from list
79
+ ace test debounce # specific question
80
+ ace test --all # run all tests
81
+ ace test --watch # watch mode (with --all)
69
82
 
70
- # Get LLM feedback
71
- ace feedback debounce
83
+ # Get LLM feedback on your solution
84
+ ace feedback # pick from list
85
+ ace feedback debounce # specific question
86
+ ace feedback --all # review all questions (confirms each one)
72
87
 
73
88
  # View scorecard
74
- ace score debounce
75
-
76
- # Reset a question
77
- ace reset debounce
89
+ ace score # pick from list
90
+ ace score debounce # specific question
91
+ ace score --all # show all scorecards
92
+
93
+ # Reset a question to its stub
94
+ ace reset # pick from list
95
+ ace reset debounce # specific question
96
+ ace reset --all # reset everything (with confirmation)
78
97
  ```
79
98
 
80
99
  ## Question Categories
81
100
 
82
- | Category | Slug | Type |
83
- |----------|------|------|
84
- | JS/TS Puzzles | `js-ts` | Coding |
85
- | Web Components | `web-components` | Coding |
86
- | React Web Apps | `react-apps` | Coding |
87
- | LeetCode Data Structures | `leetcode-ds` | Coding |
88
- | LeetCode Algorithms | `leetcode-algo` | Coding |
89
- | System Design — Frontend | `design-fe` | Design |
90
- | System Design — Backend | `design-be` | Design |
91
- | System Design — Full Stack | `design-full` | Design |
101
+ | Category | Slug | Type | Focus |
102
+ |----------|------|------|-------|
103
+ | JS/TS Puzzles | `js-ts` | Coding | Closures, async patterns, type utilities |
104
+ | React Components | `web-components` | Coding | Props, events, composition, reusable UI |
105
+ | React Web Apps | `react-apps` | Coding | Hooks, state, routing, full features |
106
+ | LeetCode Data Structures | `leetcode-ds` | Coding | Trees, graphs, heaps, hash maps |
107
+ | LeetCode Algorithms | `leetcode-algo` | Coding | DP, greedy, two pointers, sorting |
108
+ | System Design — Frontend | `design-fe` | Design | Component architecture, state, rendering |
109
+ | System Design — Backend | `design-be` | Design | APIs, databases, caching, queues |
110
+ | System Design — Full Stack | `design-full` | Design | End-to-end systems, trade-offs |
92
111
 
93
112
  ## How It Works
94
113
 
95
- 1. **Pick a question** from the dashboard (`ace list`) or generate one (`ace generate`).
114
+ 1. **Generate a question** run `ace generate` and follow the prompts (category, difficulty, topic), or use `ace generate --brainstorm` for an interactive design session.
96
115
  2. **Open the question folder** — read `README.md` for the problem statement.
97
- 3. **Write your solution** in the solution file (`solution.ts`, `App.tsx`, `component.ts`, or `notes.md`).
98
- 4. **Run tests** with `ace test <slug>` to check your work.
99
- 5. **Get feedback** with `ace feedback <slug>` for an LLM-powered code or design review.
100
- 6. **Track progress** with `ace score <slug>` and `ace list`.
116
+ 3. **Write your solution** in the solution file (`solution.ts`, `App.tsx`, `Component.tsx`, or `notes.md`).
117
+ 4. **Run tests** with `ace test` to pick a question and check your work.
118
+ 5. **Get feedback** with `ace feedback` for an LLM-powered code or design review.
119
+ 6. **Track progress** with `ace score` and `ace list`.
101
120
 
102
121
  ## Configuration
103
122
 
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import prompts from "prompts";
3
4
  import chalk from "chalk";
4
- import { findQuestion, readScorecard, writeScorecard } from "../lib/scorecard.js";
5
+ import { findQuestion, readScorecard, writeScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
5
6
  import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
6
7
  import { chatStream, requireProvider } from "../lib/llm.js";
7
8
  import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
@@ -12,35 +13,25 @@ function loadPrompt(name) {
12
13
  function parseArgs(args) {
13
14
  let slug;
14
15
  let provider;
16
+ let all = false;
15
17
  for (let i = 0; i < args.length; i++) {
16
18
  const arg = args[i];
17
19
  if (arg === "--provider" && args[i + 1]) {
18
20
  provider = args[++i];
21
+ } else if (arg === "--all" || arg === "all") {
22
+ all = true;
19
23
  } else if (!arg.startsWith("--")) {
20
24
  slug = arg;
21
25
  }
22
26
  }
23
- return { slug, provider };
27
+ return { slug, provider, all };
24
28
  }
25
- async function run(args) {
26
- const root = resolveWorkspaceRoot();
27
- if (!isWorkspaceInitialized(root)) {
28
- console.error(chalk.red("\nError: Workspace not initialized."));
29
- console.error(chalk.dim("Run `ace init` in this folder first.\n"));
30
- process.exit(1);
31
- }
32
- const parsed = parseArgs(args);
33
- if (!parsed.slug) {
34
- console.error(chalk.red("Missing question slug."));
35
- console.error(chalk.dim("Usage: npm run ace feedback <slug>"));
36
- return;
37
- }
38
- const question = findQuestion(parsed.slug);
29
+ async function runFeedbackForSlug(slug, provider) {
30
+ const question = findQuestion(slug);
39
31
  if (!question) {
40
- console.error(chalk.red(`Question not found: ${parsed.slug}`));
32
+ console.error(chalk.red(`Question not found: ${slug}`));
41
33
  return;
42
34
  }
43
- const provider = requireProvider(parsed.provider);
44
35
  const config = CATEGORIES[question.category];
45
36
  const isDesign = isDesignCategory(question.category);
46
37
  const readmePath = path.join(question.dir, "README.md");
@@ -53,7 +44,7 @@ async function run(args) {
53
44
  const notes = fs.existsSync(notesPath) ? fs.readFileSync(notesPath, "utf-8") : "";
54
45
  if (!notes.trim() || notes.includes("<!-- List the core features")) {
55
46
  console.error(chalk.yellow("Notes file appears to be empty. Write your design notes first!"));
56
- console.error(chalk.dim(`Edit: questions/${question.category}/${parsed.slug}/notes.md`));
47
+ console.error(chalk.dim(`Edit: questions/${question.category}/${slug}/notes.md`));
57
48
  return;
58
49
  }
59
50
  const designSubType = question.category === "design-fe" ? "frontend" : question.category === "design-be" ? "backend" : "full-stack";
@@ -102,7 +93,7 @@ ${solutionContent}
102
93
  ${testContent}`;
103
94
  }
104
95
  console.log(chalk.cyan(`
105
- --- LLM ${isDesign ? "Design" : "Code"} Review: ${parsed.slug} ---`));
96
+ --- LLM ${isDesign ? "Design" : "Code"} Review: ${slug} ---`));
106
97
  console.log(chalk.dim(`Provider: ${provider}
107
98
  `));
108
99
  const messages = [
@@ -116,7 +107,7 @@ ${testContent}`;
116
107
  fullResponse += chunk;
117
108
  }
118
109
  console.log("\n");
119
- const scorecard = readScorecard(question.category, parsed.slug);
110
+ const scorecard = readScorecard(question.category, slug);
120
111
  if (scorecard) {
121
112
  scorecard.llmFeedback = fullResponse;
122
113
  const scoreMatch = fullResponse.match(/Overall.*?(\d+(?:\.\d+)?)\s*\/\s*5/i);
@@ -124,10 +115,54 @@ ${testContent}`;
124
115
  const lastAttempt = scorecard.attempts[scorecard.attempts.length - 1];
125
116
  lastAttempt.llmScore = parseFloat(scoreMatch[1]);
126
117
  }
127
- writeScorecard(question.category, parsed.slug, scorecard);
118
+ writeScorecard(question.category, slug, scorecard);
128
119
  console.log(chalk.dim("Feedback saved to scorecard."));
129
120
  }
130
121
  }
122
+ async function run(args) {
123
+ const root = resolveWorkspaceRoot();
124
+ if (!isWorkspaceInitialized(root)) {
125
+ console.error(chalk.red("\nError: Workspace not initialized."));
126
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
127
+ process.exit(1);
128
+ }
129
+ const parsed = parseArgs(args);
130
+ const provider = requireProvider(parsed.provider);
131
+ if (parsed.all) {
132
+ const questions = getAllQuestions();
133
+ if (questions.length === 0) {
134
+ console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
135
+ return;
136
+ }
137
+ console.log(chalk.cyan(`
138
+ Running feedback for ${questions.length} question(s)...
139
+ `));
140
+ for (let i = 0; i < questions.length; i++) {
141
+ const q = questions[i];
142
+ console.log(chalk.bold(`
143
+ [${i + 1}/${questions.length}] ${q.slug}`));
144
+ const { confirm } = await prompts({
145
+ type: "confirm",
146
+ name: "confirm",
147
+ message: `Run feedback for "${q.slug}"?`,
148
+ initial: true
149
+ });
150
+ if (!confirm) {
151
+ console.log(chalk.dim("Skipped."));
152
+ continue;
153
+ }
154
+ await runFeedbackForSlug(q.slug, provider);
155
+ }
156
+ console.log(chalk.green("\nCompleted feedback for all questions."));
157
+ return;
158
+ }
159
+ let selectedSlug = parsed.slug;
160
+ if (!selectedSlug) {
161
+ selectedSlug = await promptForSlug();
162
+ if (!selectedSlug) return;
163
+ }
164
+ await runFeedbackForSlug(selectedSlug, provider);
165
+ }
131
166
  export {
132
167
  run
133
168
  };
@@ -56,6 +56,9 @@ Question type: ${categoryConfig.type}`;
56
56
  return;
57
57
  }
58
58
  const slug = parsed.slug || slugify(parsed.title || topic);
59
+ if (parsed.solutionCode) {
60
+ console.log(chalk.dim("Note: Discarded LLM solutionCode; using signature-based stub."));
61
+ }
59
62
  const questionDir = scaffoldQuestion({
60
63
  title: parsed.title || topic,
61
64
  slug,
@@ -63,8 +66,7 @@ Question type: ${categoryConfig.type}`;
63
66
  difficulty,
64
67
  description: parsed.description || "",
65
68
  signature: parsed.signature,
66
- testCode: parsed.testCode,
67
- solutionCode: parsed.solutionCode
69
+ testCode: parsed.testCode
68
70
  });
69
71
  console.log(chalk.green(`
70
72
  Created: questions/${category}/${slug}/`));
@@ -116,7 +118,11 @@ async function brainstormMode(provider) {
116
118
  type: "select",
117
119
  name: "category",
118
120
  message: "Which category?",
119
- choices: CATEGORY_SLUGS.map((s) => ({ title: CATEGORIES[s].name, value: s }))
121
+ choices: CATEGORY_SLUGS.map((s) => ({
122
+ title: CATEGORIES[s].name,
123
+ description: CATEGORIES[s].hint,
124
+ value: s
125
+ }))
120
126
  });
121
127
  const { difficulty } = await prompts({
122
128
  type: "select",
@@ -158,6 +164,9 @@ ${brainstormSummary}`
158
164
  return;
159
165
  }
160
166
  const slug = parsed.slug || slugify(parsed.title || "brainstorm-question");
167
+ if (parsed.solutionCode) {
168
+ console.log(chalk.dim("Note: Discarded LLM solutionCode; using signature-based stub."));
169
+ }
161
170
  const questionDir = scaffoldQuestion({
162
171
  title: parsed.title,
163
172
  slug,
@@ -165,8 +174,7 @@ ${brainstormSummary}`
165
174
  difficulty,
166
175
  description: parsed.description || "",
167
176
  signature: parsed.signature,
168
- testCode: parsed.testCode,
169
- solutionCode: parsed.solutionCode
177
+ testCode: parsed.testCode
170
178
  });
171
179
  console.log(chalk.green(`
172
180
  Created: questions/${category}/${slug}/`));
@@ -187,22 +195,23 @@ async function run(args) {
187
195
  await brainstormMode(provider);
188
196
  return;
189
197
  }
190
- if (!parsed.topic) {
191
- console.error(chalk.red("Missing --topic. Use --brainstorm for interactive mode."));
192
- console.error(chalk.dim('Example: npm run ace generate -- --topic "debounce" --category js-ts --difficulty medium'));
193
- return;
194
- }
195
198
  let category = parsed.category;
196
199
  let difficulty = parsed.difficulty;
200
+ let topic = parsed.topic;
197
201
  if (!category) {
198
202
  const result = await prompts({
199
203
  type: "select",
200
204
  name: "category",
201
205
  message: "Which category?",
202
- choices: CATEGORY_SLUGS.map((s) => ({ title: CATEGORIES[s].name, value: s }))
206
+ choices: CATEGORY_SLUGS.map((s) => ({
207
+ title: CATEGORIES[s].name,
208
+ description: CATEGORIES[s].hint,
209
+ value: s
210
+ }))
203
211
  });
204
212
  category = result.category;
205
213
  }
214
+ if (!category) return;
206
215
  if (!difficulty) {
207
216
  const result = await prompts({
208
217
  type: "select",
@@ -216,8 +225,17 @@ async function run(args) {
216
225
  });
217
226
  difficulty = result.difficulty;
218
227
  }
219
- if (!category || !difficulty) return;
220
- await directMode(provider, parsed.topic, category, difficulty);
228
+ if (!difficulty) return;
229
+ if (!topic) {
230
+ const result = await prompts({
231
+ type: "text",
232
+ name: "topic",
233
+ message: "What topic do you want to practice?"
234
+ });
235
+ topic = result.topic;
236
+ }
237
+ if (!topic) return;
238
+ await directMode(provider, topic, category, difficulty);
221
239
  }
222
240
  export {
223
241
  run
@@ -2,38 +2,16 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import prompts from "prompts";
4
4
  import chalk from "chalk";
5
- import { findQuestion, readScorecard, resetScorecard, startNewAttempt, writeScorecard } from "../lib/scorecard.js";
5
+ import { findQuestion, readScorecard, resetScorecard, startNewAttempt, writeScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
6
6
  import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
7
7
  import { getStubContent } from "../lib/scaffold.js";
8
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
- }
9
+ function resetQuestion(slug) {
22
10
  const question = findQuestion(slug);
23
11
  if (!question) {
24
12
  console.error(chalk.red(`Question not found: ${slug}`));
25
13
  return;
26
14
  }
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
15
  const config = CATEGORIES[question.category];
38
16
  const isDesign = isDesignCategory(question.category);
39
17
  if (isDesign) {
@@ -44,7 +22,6 @@ async function run(args) {
44
22
  }
45
23
  } else {
46
24
  for (const file of config.solutionFiles) {
47
- if (file === "index.html") continue;
48
25
  const filePath = path.join(question.dir, file);
49
26
  const stubContent = getStubContent(question.category, file);
50
27
  if (stubContent) {
@@ -59,8 +36,64 @@ async function run(args) {
59
36
  scorecard.status = "untouched";
60
37
  writeScorecard(question.category, slug, scorecard);
61
38
  }
62
- console.log(chalk.green(`
63
- Reset: questions/${question.category}/${slug}/`));
39
+ console.log(chalk.green(`Reset: questions/${question.category}/${slug}/`));
40
+ }
41
+ async function run(args) {
42
+ const root = resolveWorkspaceRoot();
43
+ if (!isWorkspaceInitialized(root)) {
44
+ console.error(chalk.red("\nError: Workspace not initialized."));
45
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
46
+ process.exit(1);
47
+ }
48
+ const hasAll = args.includes("--all") || args.includes("all");
49
+ const slug = args.find((a) => !a.startsWith("--") && a !== "all");
50
+ if (hasAll) {
51
+ const questions = getAllQuestions();
52
+ if (questions.length === 0) {
53
+ console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
54
+ return;
55
+ }
56
+ const { confirm: confirm2 } = await prompts({
57
+ type: "confirm",
58
+ name: "confirm",
59
+ message: `Reset ALL ${questions.length} question(s) to unanswered? This will clear all solutions.`,
60
+ initial: false
61
+ });
62
+ if (!confirm2) {
63
+ console.log(chalk.yellow("Cancelled."));
64
+ return;
65
+ }
66
+ console.log(chalk.cyan(`
67
+ Resetting ${questions.length} question(s)...
68
+ `));
69
+ for (const q of questions) {
70
+ resetQuestion(q.slug);
71
+ }
72
+ console.log(chalk.green("\nCompleted reset for all questions."));
73
+ console.log(chalk.dim("Solution files restored to stubs. Scorecards updated."));
74
+ return;
75
+ }
76
+ let selectedSlug = slug;
77
+ if (!selectedSlug) {
78
+ selectedSlug = await promptForSlug();
79
+ if (!selectedSlug) return;
80
+ }
81
+ const question = findQuestion(selectedSlug);
82
+ if (!question) {
83
+ console.error(chalk.red(`Question not found: ${selectedSlug}`));
84
+ return;
85
+ }
86
+ const { confirm } = await prompts({
87
+ type: "confirm",
88
+ name: "confirm",
89
+ message: `Reset "${selectedSlug}" to unanswered? This will clear your solution.`,
90
+ initial: false
91
+ });
92
+ if (!confirm) {
93
+ console.log(chalk.yellow("Cancelled."));
94
+ return;
95
+ }
96
+ resetQuestion(selectedSlug);
64
97
  console.log(chalk.dim("Solution files restored to stubs. Scorecard updated."));
65
98
  }
66
99
  export {
@@ -1,35 +1,13 @@
1
1
  import chalk from "chalk";
2
- import { findQuestion, readScorecard } from "../lib/scorecard.js";
2
+ import { findQuestion, readScorecard, getAllQuestions, promptForSlug } from "../lib/scorecard.js";
3
3
  import { CATEGORIES } from "../lib/categories.js";
4
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];
5
+ function displayScorecard(slug, category, scorecard) {
6
+ const config = CATEGORIES[category];
29
7
  console.log(`
30
8
  ${chalk.bold.cyan("Scorecard:")} ${chalk.bold(scorecard.title || slug)}`);
31
9
  console.log(chalk.dim("\u2500".repeat(60)));
32
- console.log(` ${chalk.bold("Category:")} ${config?.name || question.category}`);
10
+ console.log(` ${chalk.bold("Category:")} ${config?.name || category}`);
33
11
  console.log(` ${chalk.bold("Difficulty:")} ${scorecard.difficulty}`);
34
12
  console.log(` ${chalk.bold("Suggested:")} ~${scorecard.suggestedTime} minutes`);
35
13
  const statusColors = {
@@ -65,6 +43,43 @@ ${chalk.bold(" Last LLM Feedback:")}`);
65
43
  }
66
44
  console.log(chalk.dim("\n" + "\u2500".repeat(60)));
67
45
  }
46
+ async function run(args) {
47
+ const root = resolveWorkspaceRoot();
48
+ if (!isWorkspaceInitialized(root)) {
49
+ console.error(chalk.red("\nError: Workspace not initialized."));
50
+ console.error(chalk.dim("Run `ace init` in this folder first.\n"));
51
+ process.exit(1);
52
+ }
53
+ const hasAll = args.includes("--all") || args.includes("all");
54
+ const slug = args.find((a) => !a.startsWith("--") && a !== "all");
55
+ if (hasAll) {
56
+ const questions = getAllQuestions();
57
+ if (questions.length === 0) {
58
+ console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
59
+ return;
60
+ }
61
+ for (const q of questions) {
62
+ displayScorecard(q.slug, q.category, q.scorecard);
63
+ }
64
+ return;
65
+ }
66
+ let selectedSlug = slug;
67
+ if (!selectedSlug) {
68
+ selectedSlug = await promptForSlug();
69
+ if (!selectedSlug) return;
70
+ }
71
+ const question = findQuestion(selectedSlug);
72
+ if (!question) {
73
+ console.error(chalk.red(`Question not found: ${selectedSlug}`));
74
+ return;
75
+ }
76
+ const scorecard = readScorecard(question.category, selectedSlug);
77
+ if (!scorecard) {
78
+ console.error(chalk.red("No scorecard found for this question."));
79
+ return;
80
+ }
81
+ displayScorecard(selectedSlug, question.category, scorecard);
82
+ }
68
83
  export {
69
84
  run
70
85
  };
@@ -1,19 +1,22 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import chalk from "chalk";
3
- import { findQuestion, readScorecard, updateTestResults, writeScorecard } from "../lib/scorecard.js";
3
+ import { findQuestion, readScorecard, updateTestResults, writeScorecard, promptForSlug } from "../lib/scorecard.js";
4
4
  import { isDesignCategory } from "../lib/categories.js";
5
5
  import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
6
6
  function parseArgs(args) {
7
7
  let slug;
8
8
  let watch = false;
9
+ let all = false;
9
10
  for (const arg of args) {
10
11
  if (arg === "--watch") {
11
12
  watch = true;
13
+ } else if (arg === "--all" || arg === "all") {
14
+ all = true;
12
15
  } else if (!arg.startsWith("--")) {
13
16
  slug = arg;
14
17
  }
15
18
  }
16
- return { slug, watch };
19
+ return { slug, watch, all };
17
20
  }
18
21
  function parseTestOutput(output) {
19
22
  const passMatch = output.match(/(\d+)\s+passed/);
@@ -31,8 +34,8 @@ async function run(args) {
31
34
  console.error(chalk.dim("Run `ace init` in this folder first.\n"));
32
35
  process.exit(1);
33
36
  }
34
- const { slug, watch } = parseArgs(args);
35
- if (!slug) {
37
+ const { slug, watch, all } = parseArgs(args);
38
+ if (all) {
36
39
  console.log(chalk.cyan("\nRunning all tests...\n"));
37
40
  try {
38
41
  const cmd = watch ? "npx vitest" : "npx vitest run";
@@ -41,19 +44,24 @@ async function run(args) {
41
44
  }
42
45
  return;
43
46
  }
44
- const question = findQuestion(slug);
47
+ let selectedSlug = slug;
48
+ if (!selectedSlug) {
49
+ selectedSlug = await promptForSlug();
50
+ if (!selectedSlug) return;
51
+ }
52
+ const question = findQuestion(selectedSlug);
45
53
  if (!question) {
46
- console.error(chalk.red(`Question not found: ${slug}`));
54
+ console.error(chalk.red(`Question not found: ${selectedSlug}`));
47
55
  console.error(chalk.dim("Run `npm run ace list` to see all questions."));
48
56
  return;
49
57
  }
50
58
  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."));
59
+ console.log(chalk.yellow(`"${selectedSlug}" is a system design question \u2014 no tests to run.`));
60
+ console.log(chalk.dim("Use `npm run ace feedback " + selectedSlug + "` for LLM review."));
53
61
  return;
54
62
  }
55
63
  console.log(chalk.cyan(`
56
- Running tests for: ${slug}
64
+ Running tests for: ${selectedSlug}
57
65
  `));
58
66
  let output = "";
59
67
  try {
@@ -67,11 +75,11 @@ Running tests for: ${slug}
67
75
  }
68
76
  }
69
77
  if (!watch) {
70
- const scorecard = readScorecard(question.category, slug);
78
+ const scorecard = readScorecard(question.category, selectedSlug);
71
79
  if (scorecard) {
72
80
  const { total, passed } = parseTestOutput(output);
73
81
  updateTestResults(scorecard, total, passed);
74
- writeScorecard(question.category, slug, scorecard);
82
+ writeScorecard(question.category, selectedSlug, scorecard);
75
83
  if (total > 0) {
76
84
  const color = passed === total ? chalk.green : chalk.red;
77
85
  console.log(color(`
package/dist/index.js CHANGED
@@ -35,15 +35,39 @@ ${chalk.bold("Examples:")}
35
35
 
36
36
  ace setup
37
37
  ace init
38
+
39
+ ${chalk.dim("# Generate interactively (prompts for category, difficulty, topic)")}
40
+ ace generate
41
+
42
+ ${chalk.dim("# Or pass flags to skip prompts")}
38
43
  ace generate --topic "debounce" --category js-ts --difficulty medium
44
+
45
+ ${chalk.dim("# Brainstorm mode for design help")}
39
46
  ace generate --brainstorm
47
+
48
+ ${chalk.dim("# List all questions")}
40
49
  ace list
41
50
  ace list --category js-ts --status solved
51
+
52
+ ${chalk.dim("# Test interactively (shows question picker)")}
53
+ ace test
54
+
55
+ ${chalk.dim("# Or test a specific question or all questions")}
42
56
  ace test debounce
43
- ace test --watch
57
+ ace test --all
58
+
59
+ ${chalk.dim("# Feedback, score, and reset also support interactive mode and --all")}
60
+ ace feedback
44
61
  ace feedback debounce
45
- ace reset debounce
62
+ ace feedback --all
63
+
64
+ ace score
46
65
  ace score debounce
66
+ ace score --all
67
+
68
+ ace reset
69
+ ace reset debounce
70
+ ace reset --all
47
71
  `);
48
72
  }
49
73
  async function main() {
@@ -3,6 +3,7 @@ const CATEGORIES = {
3
3
  slug: "js-ts",
4
4
  name: "JS/TS Puzzles",
5
5
  shortName: "JS/TS",
6
+ hint: "Closures, async patterns, type utilities",
6
7
  type: "coding",
7
8
  suggestedTimes: { easy: 15, medium: 30, hard: 45 },
8
9
  solutionFiles: ["solution.ts"],
@@ -11,18 +12,20 @@ const CATEGORIES = {
11
12
  },
12
13
  "web-components": {
13
14
  slug: "web-components",
14
- name: "Web Components",
15
- shortName: "WebComp",
15
+ name: "React Components",
16
+ shortName: "React",
17
+ hint: "Props, events, composition, reusable UI",
16
18
  type: "coding",
17
19
  suggestedTimes: { easy: 20, medium: 35, hard: 50 },
18
- solutionFiles: ["component.ts", "index.html"],
19
- testFiles: ["component.test.ts"],
20
+ solutionFiles: ["Component.tsx"],
21
+ testFiles: ["Component.test.tsx"],
20
22
  templateDir: "web-components"
21
23
  },
22
24
  "react-apps": {
23
25
  slug: "react-apps",
24
26
  name: "React Web Apps",
25
27
  shortName: "React",
28
+ hint: "Hooks, state, routing, full features",
26
29
  type: "coding",
27
30
  suggestedTimes: { easy: 25, medium: 45, hard: 60 },
28
31
  solutionFiles: ["App.tsx"],
@@ -33,6 +36,7 @@ const CATEGORIES = {
33
36
  slug: "leetcode-ds",
34
37
  name: "LeetCode Data Structures",
35
38
  shortName: "LC-DS",
39
+ hint: "Trees, graphs, heaps, hash maps",
36
40
  type: "coding",
37
41
  suggestedTimes: { easy: 15, medium: 30, hard: 45 },
38
42
  solutionFiles: ["solution.ts"],
@@ -43,6 +47,7 @@ const CATEGORIES = {
43
47
  slug: "leetcode-algo",
44
48
  name: "LeetCode Algorithms",
45
49
  shortName: "LC-Algo",
50
+ hint: "DP, greedy, two pointers, sorting",
46
51
  type: "coding",
47
52
  suggestedTimes: { easy: 15, medium: 30, hard: 45 },
48
53
  solutionFiles: ["solution.ts"],
@@ -53,6 +58,7 @@ const CATEGORIES = {
53
58
  slug: "design-fe",
54
59
  name: "System Design \u2014 Frontend",
55
60
  shortName: "Design-FE",
61
+ hint: "Component architecture, state, rendering",
56
62
  type: "design",
57
63
  suggestedTimes: { easy: 25, medium: 40, hard: 55 },
58
64
  solutionFiles: ["notes.md"],
@@ -63,6 +69,7 @@ const CATEGORIES = {
63
69
  slug: "design-be",
64
70
  name: "System Design \u2014 Backend",
65
71
  shortName: "Design-BE",
72
+ hint: "APIs, databases, caching, queues",
66
73
  type: "design",
67
74
  suggestedTimes: { easy: 25, medium: 40, hard: 55 },
68
75
  solutionFiles: ["notes.md"],
@@ -73,6 +80,7 @@ const CATEGORIES = {
73
80
  slug: "design-full",
74
81
  name: "System Design \u2014 Full Stack",
75
82
  shortName: "Design-Full",
83
+ hint: "End-to-end systems, trade-offs",
76
84
  type: "design",
77
85
  suggestedTimes: { easy: 30, medium: 45, hard: 60 },
78
86
  solutionFiles: ["notes.md"],
@@ -37,8 +37,7 @@ function scaffoldQuestion(opts) {
37
37
  description: opts.description,
38
38
  signature: opts.signature || "",
39
39
  testCode: opts.testCode || "",
40
- solutionCode: opts.solutionCode || "",
41
- htmlCode: opts.htmlCode || ""
40
+ solutionCode: opts.solutionCode || ""
42
41
  };
43
42
  const readmeTemplate = loadTemplate(path.join(TEMPLATES_DIR, "readme.md.hbs"));
44
43
  fs.writeFileSync(path.join(questionDir, "README.md"), readmeTemplate(templateData));
@@ -67,15 +66,6 @@ function scaffoldQuestion(opts) {
67
66
  fs.writeFileSync(path.join(questionDir, testFile), opts.testCode);
68
67
  }
69
68
  }
70
- if (config.solutionFiles.includes("index.html") && opts.htmlCode) {
71
- fs.writeFileSync(path.join(questionDir, "index.html"), opts.htmlCode);
72
- } else {
73
- const htmlTemplatePath = path.join(templateDir, "index.html.hbs");
74
- if (fs.existsSync(htmlTemplatePath) && config.solutionFiles.includes("index.html")) {
75
- const tmpl = loadTemplate(htmlTemplatePath);
76
- fs.writeFileSync(path.join(questionDir, "index.html"), tmpl(templateData));
77
- }
78
- }
79
69
  }
80
70
  const scorecard = createScorecard(opts.title, opts.category, opts.difficulty);
81
71
  writeScorecard(opts.category, opts.slug, scorecard);
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { getSuggestedTime } from "./categories.js";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { getSuggestedTime, CATEGORIES } from "./categories.js";
4
6
  import { resolveWorkspaceRoot, getQuestionsDir } from "./paths.js";
5
7
  function getScorecardPath(category, slug) {
6
8
  const root = resolveWorkspaceRoot();
@@ -102,12 +104,41 @@ function findQuestion(slug) {
102
104
  }
103
105
  return null;
104
106
  }
107
+ async function promptForSlug() {
108
+ const questions = getAllQuestions();
109
+ if (questions.length === 0) {
110
+ console.log(chalk.yellow("No questions found. Create one first with `ace generate` or `ace add`."));
111
+ return null;
112
+ }
113
+ const choices = questions.map((q) => {
114
+ const categoryName = CATEGORIES[q.category]?.shortName || q.category;
115
+ const statusColors = {
116
+ untouched: chalk.gray,
117
+ "in-progress": chalk.yellow,
118
+ attempted: chalk.red,
119
+ solved: chalk.green
120
+ };
121
+ const statusColor = statusColors[q.scorecard.status] || chalk.white;
122
+ return {
123
+ title: `${chalk.cyan(categoryName)} / ${q.slug} ${statusColor(`(${q.scorecard.status})`)}`,
124
+ value: q.slug
125
+ };
126
+ });
127
+ const result = await prompts({
128
+ type: "select",
129
+ name: "slug",
130
+ message: "Select a question:",
131
+ choices
132
+ });
133
+ return result.slug || null;
134
+ }
105
135
  export {
106
136
  createScorecard,
107
137
  findQuestion,
108
138
  getAllQuestions,
109
139
  getCurrentAttempt,
110
140
  getScorecardPath,
141
+ promptForSlug,
111
142
  readScorecard,
112
143
  resetScorecard,
113
144
  startNewAttempt,
@@ -5,8 +5,8 @@ You are a collaborative interview question designer helping the user explore and
5
5
  ## Supported Categories
6
6
 
7
7
  - **js-ts**: JavaScript/TypeScript puzzles (closures, async, types)
8
- - **web-components**: Custom elements, Shadow DOM, lifecycle
9
- - **react-apps**: React components, hooks, state, rendering
8
+ - **web-components**: React components (props, events, composition, reusable UI)
9
+ - **react-apps**: React applications (hooks, state, routing, full features)
10
10
  - **leetcode-ds**: Data structure problems (trees, graphs, heaps)
11
11
  - **leetcode-algo**: Algorithm problems (DP, greedy, two pointers)
12
12
  - **design-fe**: Frontend system design (component architecture, state, rendering)
@@ -22,26 +22,35 @@ Return a JSON object with:
22
22
  "title": "Human-readable question title",
23
23
  "slug": "kebab-case-slug",
24
24
  "description": "Markdown description with problem statement, examples, constraints",
25
- "signature": "Function signature or component interface the candidate must implement",
26
- "testCode": "Full Vitest test file content as a string",
27
- "solutionCode": "Stub implementation with the signature only (empty body or minimal placeholder)"
25
+ "signature": "The exported function/class signature line ONLY (see rules below)",
26
+ "testCode": "Full Vitest test file content as a string"
28
27
  }
29
28
  ```
30
29
 
30
+ **CRITICAL — Do NOT include `solutionCode` in your response. Do NOT implement the solution.**
31
+
32
+ The `signature` field must contain ONLY the bare function or class declaration line that the candidate will implement. It must NOT contain any logic, algorithm, data structure manipulation, loops, conditionals, or meaningful code.
33
+
34
+ Good `signature` examples:
35
+ - `export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): T`
36
+ - `export function deepClone<T>(obj: T): T`
37
+ - `export class LRUCache<K, V>`
38
+
39
+ Bad `signature` examples (NEVER do this):
40
+ - A full function body with implementation logic
41
+ - Code that includes `if`, `for`, `while`, `map`, `reduce`, `setTimeout`, or any working logic
42
+ - A complete class with method implementations
43
+
31
44
  **Test requirements:**
32
45
  - Generate 6–10 test cases covering: happy path, edge cases, and performance-sensitive scenarios
33
- - For **React** questions: use `@testing-library/react` with `render` and `screen`
46
+ - For **React** questions (`react-apps`, `web-components`): use `@testing-library/react` with `render` and `screen`
34
47
  - Imports must reference the solution file correctly:
35
48
  - `js-ts`, `leetcode-ds`, `leetcode-algo`: `import { solution } from './solution'`
36
49
  - `react-apps`: `import App from './App'`
37
- - `web-components`: import the component from the appropriate file (e.g., `'./component'`)
50
+ - `web-components`: `import { ComponentName } from './Component'` (named export)
38
51
  - Use `describe`, `it`, `expect` from Vitest
39
52
  - Tests must be self-contained and runnable
40
53
 
41
- **Solution stub:**
42
- - Include the exact function/component signature the candidate must implement
43
- - Leave the body empty or with a minimal placeholder (e.g., `throw new Error('Not implemented')` or `return null`)
44
-
45
54
  ### For Design Categories (design-fe, design-be, design-full)
46
55
 
47
56
  Return a JSON object with:
@@ -62,4 +71,4 @@ Return a JSON object with:
62
71
  - Questions should be achievable within the suggested time for the category and difficulty
63
72
  - Avoid ambiguous wording; constraints and expected behavior should be explicit
64
73
  - For LeetCode-style questions: include time/space complexity expectations in the description
65
- - For React/Web Components: focus on realistic UI behavior, not toy examples
74
+ - For React questions (`react-apps`, `web-components`): focus on realistic UI behavior, not toy examples
@@ -0,0 +1,16 @@
1
+ {{#if testCode}}
2
+ {{{testCode}}}
3
+ {{else}}
4
+ import { describe, it, expect } from 'vitest';
5
+ import { render, screen } from '@testing-library/react';
6
+ import { Component } from './Component';
7
+
8
+ describe('{{title}}', () => {
9
+ it('renders without crashing', () => {
10
+ render(<Component />);
11
+ expect(screen.getByText('{{title}}')).toBeDefined();
12
+ });
13
+
14
+ it.todo('add more test cases');
15
+ });
16
+ {{/if}}
@@ -0,0 +1,29 @@
1
+ {{#if solutionCode}}
2
+ {{{solutionCode}}}
3
+ {{else}}
4
+ {{#if signature}}
5
+ import React from 'react';
6
+
7
+ {{{signature}}} {
8
+ return (
9
+ <div>
10
+ <h1>{{title}}</h1>
11
+ {/* TODO: implement */}
12
+ </div>
13
+ );
14
+ }
15
+ {{else}}
16
+ import React from 'react';
17
+
18
+ // TODO: implement your React component here
19
+
20
+ export function Component() {
21
+ return (
22
+ <div>
23
+ <h1>{{title}}</h1>
24
+ {/* TODO: implement */}
25
+ </div>
26
+ );
27
+ }
28
+ {{/if}}
29
+ {{/if}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ace-interview-prep",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool for frontend interview preparation — scaffolds questions with tests, tracks progress, and provides LLM-powered feedback",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,11 +0,0 @@
1
- {{#if testCode}}
2
- {{{testCode}}}
3
- {{else}}
4
- import { describe, it, expect } from 'vitest';
5
- // TODO: import your component
6
- // import './component';
7
-
8
- describe('{{title}}', () => {
9
- it.todo('add test cases');
10
- });
11
- {{/if}}
@@ -1,22 +0,0 @@
1
- {{#if solutionCode}}
2
- {{{solutionCode}}}
3
- {{else}}
4
- {{#if signature}}
5
- {{{signature}}}
6
- {{else}}
7
- // TODO: implement your web component here
8
-
9
- export class MyComponent extends HTMLElement {
10
- constructor() {
11
- super();
12
- this.attachShadow({ mode: 'open' });
13
- }
14
-
15
- connectedCallback() {
16
- // TODO: implement
17
- }
18
- }
19
-
20
- // customElements.define('my-component', MyComponent);
21
- {{/if}}
22
- {{/if}}
@@ -1,12 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>{{title}}</title>
7
- </head>
8
- <body>
9
- <!-- TODO: use your web component here -->
10
- <script type="module" src="./component.ts"></script>
11
- </body>
12
- </html>
@@ -1,45 +0,0 @@
1
- # Build a Star Rating Web Component
2
-
3
- **Category:** Web Components
4
- **Difficulty:** Medium
5
- **Suggested Time:** ~35 minutes
6
-
7
- ---
8
-
9
- ## Problem
10
-
11
- Build a `<star-rating>` custom element that displays 5 stars, allows users to click to rate, has a `value` attribute/property, and dispatches a `change` event when the rating changes.
12
-
13
- ## Requirements
14
-
15
- - **Display** — Render 5 star elements (you may use Unicode stars ★/☆, SVG, or styled spans).
16
- - **Click to rate** — Clicking a star sets the rating to that star's index (1–5).
17
- - **`value` attribute** — The component accepts a `value` attribute (e.g. `<star-rating value="3">`) to show the initial or current rating.
18
- - **`value` property** — The component exposes a `value` getter/setter that reflects and updates the rating.
19
- - **`change` event** — When the user clicks a star, dispatch a `change` event with the new value (e.g. `detail: { value: 3 }`).
20
-
21
- ## Example Usage
22
-
23
- ```html
24
- <star-rating value="3"></star-rating>
25
- ```
26
-
27
- ```js
28
- const el = document.querySelector('star-rating');
29
- el.value = 4;
30
- el.addEventListener('change', (e) => console.log('New rating:', e.detail.value));
31
- ```
32
-
33
- ## Constraints
34
-
35
- - Use the Custom Elements API (extend `HTMLElement`).
36
- - Use Shadow DOM for encapsulation.
37
- - Observe the `value` attribute and sync it with the internal state.
38
- - Clamp `value` to 0–5 (0 = no stars selected).
39
-
40
- ## Hints
41
-
42
- - Use `attachShadow({ mode: 'open' })` in the constructor.
43
- - Use `static get observedAttributes()` to return `['value']`.
44
- - In `attributeChangedCallback`, parse the attribute and update the display.
45
- - Use `CustomEvent` with `detail: { value }` for the change event.
@@ -1,64 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import './component';
3
-
4
- describe('star-rating', () => {
5
- let el: HTMLElement & { value: number };
6
-
7
- beforeEach(() => {
8
- el = document.createElement('star-rating') as HTMLElement & { value: number };
9
- document.body.appendChild(el);
10
- });
11
-
12
- afterEach(() => {
13
- el.remove();
14
- });
15
-
16
- it('renders 5 star elements', () => {
17
- const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
18
- expect(stars.length).toBe(5);
19
- });
20
-
21
- it('default value is 0', () => {
22
- expect(el.value).toBe(0);
23
- });
24
-
25
- it('setting value attribute updates display', () => {
26
- el.setAttribute('value', '3');
27
- expect(el.value).toBe(3);
28
- });
29
-
30
- it('clicking a star updates value', () => {
31
- const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
32
- (stars[2] as HTMLElement).click();
33
- expect(el.value).toBe(3);
34
- });
35
-
36
- it('dispatches change event on click', () => {
37
- let receivedValue: number | undefined;
38
- const handler = (e: Event) => {
39
- receivedValue = (e as CustomEvent).detail?.value;
40
- };
41
- el.addEventListener('change', handler);
42
-
43
- const stars = el.shadowRoot?.querySelectorAll('[data-star]') ?? [];
44
- (stars[3] as HTMLElement).click();
45
-
46
- expect(receivedValue).toBe(4);
47
- el.removeEventListener('change', handler);
48
- });
49
-
50
- it('value property reflects attribute', () => {
51
- el.setAttribute('value', '2');
52
- expect(el.value).toBe(2);
53
- });
54
-
55
- it('setting value property updates attribute', () => {
56
- el.value = 5;
57
- expect(el.getAttribute('value')).toBe('5');
58
- });
59
-
60
- it('clamps value to 0-5', () => {
61
- el.setAttribute('value', '10');
62
- expect(el.value).toBeLessThanOrEqual(5);
63
- });
64
- });
@@ -1,28 +0,0 @@
1
- export class StarRating extends HTMLElement {
2
- constructor() {
3
- super();
4
- this.attachShadow({ mode: 'open' });
5
- }
6
-
7
- connectedCallback() {
8
- // TODO: implement - render 5 stars, handle click, support value attribute
9
- }
10
-
11
- static get observedAttributes() {
12
- return ['value'];
13
- }
14
-
15
- attributeChangedCallback(_name: string, _oldValue: string | null, _newValue: string | null) {
16
- // TODO: implement
17
- }
18
-
19
- get value(): number {
20
- return 0; // TODO: implement
21
- }
22
-
23
- set value(_val: number) {
24
- // TODO: implement
25
- }
26
- }
27
-
28
- customElements.define('star-rating', StarRating);
@@ -1,14 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Star Rating Component</title>
7
- </head>
8
- <body>
9
- <h1>Star Rating</h1>
10
- <star-rating value="3"></star-rating>
11
-
12
- <script type="module" src="./component.ts"></script>
13
- </body>
14
- </html>
@@ -1,9 +0,0 @@
1
- {
2
- "title": "Build a Star Rating Component",
3
- "category": "web-components",
4
- "difficulty": "medium",
5
- "suggestedTime": 35,
6
- "status": "untouched",
7
- "attempts": [],
8
- "llmFeedback": null
9
- }