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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # ace
2
+
3
+ [![npm version](https://img.shields.io/npm/v/ace-interview-prep.svg)](https://www.npmjs.com/package/ace-interview-prep)
4
+ [![CI](https://github.com/neel/ace-interview-prep/actions/workflows/ci.yml/badge.svg)](https://github.com/neel/ace-interview-prep/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ A CLI tool for staff-engineer-level frontend interview preparation. Scaffolds questions with test cases, tracks progress with scorecards, and provides LLM-powered feedback.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g ace-interview-prep
13
+ ```
14
+
15
+ Or run directly with npx:
16
+
17
+ ```bash
18
+ npx ace-interview-prep help
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ### 1. Configure API Keys
24
+
25
+ ```bash
26
+ ace setup
27
+ ```
28
+
29
+ Stores your OpenAI / Anthropic API keys in `~/.ace/config.json` (one-time, works across all workspaces).
30
+
31
+ ```bash
32
+ # Non-interactive
33
+ ace setup --openai-key sk-... --anthropic-key sk-ant-...
34
+ ```
35
+
36
+ ### 2. Initialize a Workspace
37
+
38
+ Navigate to any folder where you want to practice:
39
+
40
+ ```bash
41
+ ace init
42
+ ```
43
+
44
+ Creates a `questions/` directory and vitest config files. Then install the test dependencies:
45
+
46
+ ```bash
47
+ npm install vitest happy-dom @testing-library/jest-dom
48
+ ```
49
+
50
+ ### 3. Practice
51
+
52
+ ```bash
53
+ # Generate a question via LLM
54
+ ace generate --topic "debounce" --category js-ts --difficulty medium
55
+
56
+ # Interactive brainstorm mode
57
+ ace generate --brainstorm
58
+
59
+ # Manually add a question
60
+ ace add
61
+
62
+ # List all questions
63
+ ace list
64
+
65
+ # Run tests
66
+ ace test debounce
67
+ ace test # run all
68
+ ace test --watch # watch mode
69
+
70
+ # Get LLM feedback
71
+ ace feedback debounce
72
+
73
+ # View scorecard
74
+ ace score debounce
75
+
76
+ # Reset a question
77
+ ace reset debounce
78
+ ```
79
+
80
+ ## Question Categories
81
+
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 |
92
+
93
+ ## How It Works
94
+
95
+ 1. **Pick a question** from the dashboard (`ace list`) or generate one (`ace generate`).
96
+ 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`.
101
+
102
+ ## Configuration
103
+
104
+ **Global** (`~/.ace/`) — API keys stored once, shared across all workspaces.
105
+
106
+ - `~/.ace/config.json` — primary config (created by `ace setup`)
107
+ - `~/.ace/.env` — fallback (dotenv format)
108
+ - Environment variables — final fallback
109
+
110
+ **Workspace** — each workspace gets its own `questions/` directory and test config.
111
+
112
+ ## Seed Questions
113
+
114
+ Ships with 8 starter questions (one per category) so you can begin practicing immediately after install.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ git clone https://github.com/neel/ace-interview-prep.git
120
+ cd ace-interview-prep
121
+ npm install
122
+ npm run ace help
123
+ ```
124
+
125
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development guide.
126
+
127
+ ## License
128
+
129
+ [MIT](LICENSE)
@@ -0,0 +1,92 @@
1
+ import prompts from "prompts";
2
+ import chalk from "chalk";
3
+ import { CATEGORIES, CATEGORY_SLUGS, slugify } from "../lib/categories.js";
4
+ import { scaffoldQuestion } from "../lib/scaffold.js";
5
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
6
+ async function run(_args) {
7
+ const root = resolveWorkspaceRoot();
8
+ if (!isWorkspaceInitialized(root)) {
9
+ console.log(chalk.yellow("\nWorkspace not initialized. Running init...\n"));
10
+ const initModule = await import("./init.js");
11
+ await initModule.run([]);
12
+ console.log();
13
+ }
14
+ console.log(chalk.cyan("\n--- Add a Question Manually ---\n"));
15
+ const { category } = await prompts({
16
+ type: "select",
17
+ name: "category",
18
+ message: "Category:",
19
+ choices: CATEGORY_SLUGS.map((s) => ({ title: CATEGORIES[s].name, value: s }))
20
+ });
21
+ if (!category) return;
22
+ const { difficulty } = await prompts({
23
+ type: "select",
24
+ name: "difficulty",
25
+ message: "Difficulty:",
26
+ choices: [
27
+ { title: "Easy", value: "easy" },
28
+ { title: "Medium", value: "medium" },
29
+ { title: "Hard", value: "hard" }
30
+ ]
31
+ });
32
+ if (!difficulty) return;
33
+ const { title } = await prompts({
34
+ type: "text",
35
+ name: "title",
36
+ message: "Question title:",
37
+ validate: (v) => v.length > 0 ? true : "Title is required"
38
+ });
39
+ if (!title) return;
40
+ const slug = slugify(title);
41
+ const { description } = await prompts({
42
+ type: "text",
43
+ name: "description",
44
+ message: "Problem description (paste markdown, press Enter when done):"
45
+ });
46
+ const config = CATEGORIES[category];
47
+ let signature;
48
+ let testCode;
49
+ let solutionCode;
50
+ if (config.type === "coding") {
51
+ const sigResult = await prompts({
52
+ type: "text",
53
+ name: "signature",
54
+ message: "Function/component signature (optional):"
55
+ });
56
+ signature = sigResult.signature || void 0;
57
+ const testResult = await prompts({
58
+ type: "text",
59
+ name: "testCode",
60
+ message: "Paste test code (optional, press Enter to skip):"
61
+ });
62
+ testCode = testResult.testCode || void 0;
63
+ const solResult = await prompts({
64
+ type: "text",
65
+ name: "solutionCode",
66
+ message: "Paste starter/solution code (optional, press Enter to skip):"
67
+ });
68
+ solutionCode = solResult.solutionCode || void 0;
69
+ }
70
+ try {
71
+ const questionDir = scaffoldQuestion({
72
+ title,
73
+ slug,
74
+ category,
75
+ difficulty,
76
+ description: description || "",
77
+ signature,
78
+ testCode,
79
+ solutionCode
80
+ });
81
+ console.log(chalk.green(`
82
+ Created: questions/${category}/${slug}/`));
83
+ console.log(chalk.dim(` ${questionDir}`));
84
+ } catch (err) {
85
+ if (err instanceof Error) {
86
+ console.error(chalk.red(err.message));
87
+ }
88
+ }
89
+ }
90
+ export {
91
+ run
92
+ };
@@ -0,0 +1,133 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { findQuestion, readScorecard, writeScorecard } from "../lib/scorecard.js";
5
+ import { CATEGORIES, isDesignCategory } from "../lib/categories.js";
6
+ import { chatStream, requireProvider } from "../lib/llm.js";
7
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
8
+ const PROMPTS_DIR = path.resolve(import.meta.dirname, "../prompts");
9
+ function loadPrompt(name) {
10
+ return fs.readFileSync(path.join(PROMPTS_DIR, name), "utf-8");
11
+ }
12
+ function parseArgs(args) {
13
+ let slug;
14
+ let provider;
15
+ for (let i = 0; i < args.length; i++) {
16
+ const arg = args[i];
17
+ if (arg === "--provider" && args[i + 1]) {
18
+ provider = args[++i];
19
+ } else if (!arg.startsWith("--")) {
20
+ slug = arg;
21
+ }
22
+ }
23
+ return { slug, provider };
24
+ }
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);
39
+ if (!question) {
40
+ console.error(chalk.red(`Question not found: ${parsed.slug}`));
41
+ return;
42
+ }
43
+ const provider = requireProvider(parsed.provider);
44
+ const config = CATEGORIES[question.category];
45
+ const isDesign = isDesignCategory(question.category);
46
+ const readmePath = path.join(question.dir, "README.md");
47
+ const readme = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, "utf-8") : "";
48
+ let systemPrompt;
49
+ let userContent;
50
+ if (isDesign) {
51
+ systemPrompt = loadPrompt("design-review.md");
52
+ const notesPath = path.join(question.dir, "notes.md");
53
+ const notes = fs.existsSync(notesPath) ? fs.readFileSync(notesPath, "utf-8") : "";
54
+ if (!notes.trim() || notes.includes("<!-- List the core features")) {
55
+ 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`));
57
+ return;
58
+ }
59
+ const designSubType = question.category === "design-fe" ? "frontend" : question.category === "design-be" ? "backend" : "full-stack";
60
+ userContent = `## Design Sub-Type: ${designSubType}
61
+
62
+ ## Problem Statement
63
+ ${readme}
64
+
65
+ ## Candidate's Design Notes
66
+ ${notes}`;
67
+ } else {
68
+ systemPrompt = loadPrompt("code-review.md");
69
+ const solutionFiles = config.solutionFiles;
70
+ let solutionContent = "";
71
+ for (const f of solutionFiles) {
72
+ const fp = path.join(question.dir, f);
73
+ if (fs.existsSync(fp)) {
74
+ const content = fs.readFileSync(fp, "utf-8");
75
+ solutionContent += `
76
+ --- ${f} ---
77
+ ${content}
78
+ `;
79
+ }
80
+ }
81
+ if (!solutionContent.trim() || solutionContent.includes("// TODO: implement")) {
82
+ console.error(chalk.yellow("Solution appears to be the default stub. Write your solution first!"));
83
+ return;
84
+ }
85
+ let testContent = "";
86
+ for (const f of config.testFiles) {
87
+ const fp = path.join(question.dir, f);
88
+ if (fs.existsSync(fp)) {
89
+ testContent += `
90
+ --- ${f} ---
91
+ ${fs.readFileSync(fp, "utf-8")}
92
+ `;
93
+ }
94
+ }
95
+ userContent = `## Problem Statement
96
+ ${readme}
97
+
98
+ ## Candidate's Solution Code
99
+ ${solutionContent}
100
+
101
+ ## Test Cases
102
+ ${testContent}`;
103
+ }
104
+ console.log(chalk.cyan(`
105
+ --- LLM ${isDesign ? "Design" : "Code"} Review: ${parsed.slug} ---`));
106
+ console.log(chalk.dim(`Provider: ${provider}
107
+ `));
108
+ const messages = [
109
+ { role: "system", content: systemPrompt },
110
+ { role: "user", content: userContent }
111
+ ];
112
+ const stream = await chatStream(provider, messages);
113
+ let fullResponse = "";
114
+ for await (const chunk of stream) {
115
+ process.stdout.write(chunk);
116
+ fullResponse += chunk;
117
+ }
118
+ console.log("\n");
119
+ const scorecard = readScorecard(question.category, parsed.slug);
120
+ if (scorecard) {
121
+ scorecard.llmFeedback = fullResponse;
122
+ const scoreMatch = fullResponse.match(/Overall.*?(\d+(?:\.\d+)?)\s*\/\s*5/i);
123
+ if (scoreMatch && scorecard.attempts.length > 0) {
124
+ const lastAttempt = scorecard.attempts[scorecard.attempts.length - 1];
125
+ lastAttempt.llmScore = parseFloat(scoreMatch[1]);
126
+ }
127
+ writeScorecard(question.category, parsed.slug, scorecard);
128
+ console.log(chalk.dim("Feedback saved to scorecard."));
129
+ }
130
+ }
131
+ export {
132
+ run
133
+ };
@@ -0,0 +1,224 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { CATEGORIES, CATEGORY_SLUGS, slugify } from "../lib/categories.js";
6
+ import { chat, chatStream, requireProvider } from "../lib/llm.js";
7
+ import { scaffoldQuestion } from "../lib/scaffold.js";
8
+ import { resolveWorkspaceRoot, isWorkspaceInitialized } from "../lib/paths.js";
9
+ const PROMPTS_DIR = path.resolve(import.meta.dirname, "../prompts");
10
+ function parseArgs(args) {
11
+ const result = {};
12
+ for (let i = 0; i < args.length; i++) {
13
+ const arg = args[i];
14
+ if (arg.startsWith("--")) {
15
+ const key = arg.slice(2);
16
+ const next = args[i + 1];
17
+ if (next && !next.startsWith("--")) {
18
+ result[key] = next;
19
+ i++;
20
+ } else {
21
+ result[key] = "true";
22
+ }
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+ function loadPrompt(name) {
28
+ return fs.readFileSync(path.join(PROMPTS_DIR, name), "utf-8");
29
+ }
30
+ function extractJSON(text) {
31
+ const match = text.match(/```json\s*([\s\S]*?)```/);
32
+ if (match) return match[1].trim();
33
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
34
+ if (jsonMatch) return jsonMatch[0];
35
+ return text;
36
+ }
37
+ async function directMode(provider, topic, category, difficulty) {
38
+ const systemPrompt = loadPrompt("question-generate.md");
39
+ const categoryConfig = CATEGORIES[category];
40
+ console.log(chalk.cyan(`
41
+ Generating ${categoryConfig.name} question: "${topic}" (${difficulty})...`));
42
+ const userMessage = `Generate a ${difficulty} difficulty ${categoryConfig.name} interview question about: ${topic}
43
+
44
+ Category slug: ${category}
45
+ Question type: ${categoryConfig.type}`;
46
+ const response = await chat(provider, [
47
+ { role: "system", content: systemPrompt },
48
+ { role: "user", content: userMessage }
49
+ ], true);
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(extractJSON(response));
53
+ } catch {
54
+ console.error(chalk.red("Failed to parse LLM response as JSON. Raw response:"));
55
+ console.error(response);
56
+ return;
57
+ }
58
+ const slug = parsed.slug || slugify(parsed.title || topic);
59
+ const questionDir = scaffoldQuestion({
60
+ title: parsed.title || topic,
61
+ slug,
62
+ category,
63
+ difficulty,
64
+ description: parsed.description || "",
65
+ signature: parsed.signature,
66
+ testCode: parsed.testCode,
67
+ solutionCode: parsed.solutionCode
68
+ });
69
+ console.log(chalk.green(`
70
+ Created: questions/${category}/${slug}/`));
71
+ console.log(chalk.dim(` ${questionDir}`));
72
+ }
73
+ async function brainstormMode(provider) {
74
+ const systemPrompt = loadPrompt("question-brainstorm.md");
75
+ const messages = [
76
+ { role: "system", content: systemPrompt }
77
+ ];
78
+ console.log(chalk.cyan("\n--- Brainstorm Mode ---"));
79
+ console.log(chalk.dim('Chat with the LLM to design a question. Type "done" when ready to scaffold.\n'));
80
+ const { area } = await prompts({
81
+ type: "text",
82
+ name: "area",
83
+ message: "What area do you want to practice?"
84
+ });
85
+ if (!area) return;
86
+ messages.push({
87
+ role: "user",
88
+ content: `I want to practice: ${area}. Suggest some question directions across relevant categories.`
89
+ });
90
+ while (true) {
91
+ console.log(chalk.dim("\nThinking...\n"));
92
+ const stream = await chatStream(provider, messages);
93
+ let fullResponse = "";
94
+ for await (const chunk of stream) {
95
+ process.stdout.write(chunk);
96
+ fullResponse += chunk;
97
+ }
98
+ console.log("\n");
99
+ messages.push({ role: "assistant", content: fullResponse });
100
+ const { userInput } = await prompts({
101
+ type: "text",
102
+ name: "userInput",
103
+ message: chalk.dim('Your response (or "done" to scaffold, "quit" to exit):')
104
+ });
105
+ if (!userInput || userInput.toLowerCase() === "quit" || userInput.toLowerCase() === "q") {
106
+ console.log(chalk.yellow("Exiting brainstorm."));
107
+ return;
108
+ }
109
+ if (userInput.toLowerCase() === "done" || userInput.toLowerCase() === "y") {
110
+ break;
111
+ }
112
+ messages.push({ role: "user", content: userInput });
113
+ }
114
+ console.log(chalk.cyan("\nNow let me finalize the question..."));
115
+ const { category } = await prompts({
116
+ type: "select",
117
+ name: "category",
118
+ message: "Which category?",
119
+ choices: CATEGORY_SLUGS.map((s) => ({ title: CATEGORIES[s].name, value: s }))
120
+ });
121
+ const { difficulty } = await prompts({
122
+ type: "select",
123
+ name: "difficulty",
124
+ message: "Difficulty?",
125
+ choices: [
126
+ { title: "Easy", value: "easy" },
127
+ { title: "Medium", value: "medium" },
128
+ { title: "Hard", value: "hard" }
129
+ ]
130
+ });
131
+ if (!category || !difficulty) return;
132
+ const generatePrompt = loadPrompt("question-generate.md");
133
+ const brainstormSummary = messages.filter((m) => m.role !== "system").map((m) => `${m.role}: ${m.content}`).join("\n\n");
134
+ const categoryConfig = CATEGORIES[category];
135
+ const response = await chat(
136
+ provider,
137
+ [
138
+ { role: "system", content: generatePrompt },
139
+ {
140
+ role: "user",
141
+ content: `Based on the following brainstorm conversation, generate a structured ${difficulty} ${categoryConfig.name} interview question.
142
+
143
+ Category slug: ${category}
144
+ Question type: ${categoryConfig.type}
145
+
146
+ Brainstorm conversation:
147
+ ${brainstormSummary}`
148
+ }
149
+ ],
150
+ true
151
+ );
152
+ let parsed;
153
+ try {
154
+ parsed = JSON.parse(extractJSON(response));
155
+ } catch {
156
+ console.error(chalk.red("Failed to parse LLM response. Raw:"));
157
+ console.error(response);
158
+ return;
159
+ }
160
+ const slug = parsed.slug || slugify(parsed.title || "brainstorm-question");
161
+ const questionDir = scaffoldQuestion({
162
+ title: parsed.title,
163
+ slug,
164
+ category,
165
+ difficulty,
166
+ description: parsed.description || "",
167
+ signature: parsed.signature,
168
+ testCode: parsed.testCode,
169
+ solutionCode: parsed.solutionCode
170
+ });
171
+ console.log(chalk.green(`
172
+ Created: questions/${category}/${slug}/`));
173
+ console.log(chalk.dim(` ${questionDir}`));
174
+ }
175
+ async function run(args) {
176
+ const parsed = parseArgs(args);
177
+ const root = resolveWorkspaceRoot();
178
+ if (!isWorkspaceInitialized(root)) {
179
+ console.log(chalk.yellow("\nWorkspace not initialized. Running init...\n"));
180
+ const initModule = await import("./init.js");
181
+ await initModule.run([]);
182
+ console.log();
183
+ }
184
+ const provider = requireProvider(parsed.provider);
185
+ console.log(chalk.dim(`Using LLM provider: ${provider}`));
186
+ if (parsed.brainstorm === "true") {
187
+ await brainstormMode(provider);
188
+ return;
189
+ }
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
+ let category = parsed.category;
196
+ let difficulty = parsed.difficulty;
197
+ if (!category) {
198
+ const result = await prompts({
199
+ type: "select",
200
+ name: "category",
201
+ message: "Which category?",
202
+ choices: CATEGORY_SLUGS.map((s) => ({ title: CATEGORIES[s].name, value: s }))
203
+ });
204
+ category = result.category;
205
+ }
206
+ if (!difficulty) {
207
+ const result = await prompts({
208
+ type: "select",
209
+ name: "difficulty",
210
+ message: "Difficulty?",
211
+ choices: [
212
+ { title: "Easy", value: "easy" },
213
+ { title: "Medium", value: "medium" },
214
+ { title: "Hard", value: "hard" }
215
+ ]
216
+ });
217
+ difficulty = result.difficulty;
218
+ }
219
+ if (!category || !difficulty) return;
220
+ await directMode(provider, parsed.topic, category, difficulty);
221
+ }
222
+ export {
223
+ run
224
+ };
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { getQuestionsDir, isWorkspaceInitialized } from "../lib/paths.js";
5
+ function parseArgs(args) {
6
+ return {
7
+ force: args.includes("--force"),
8
+ writeScripts: args.includes("--write-scripts")
9
+ };
10
+ }
11
+ const VITEST_CONFIG_TEMPLATE = `import { defineConfig } from 'vitest/config';
12
+
13
+ export default defineConfig({
14
+ test: {
15
+ globals: true,
16
+ environment: 'happy-dom',
17
+ include: ['questions/**/*.test.{ts,tsx}'],
18
+ testTimeout: 10000,
19
+ setupFiles: ['vitest.setup.ts'],
20
+ },
21
+ });
22
+ `;
23
+ const VITEST_SETUP_TEMPLATE = `import '@testing-library/jest-dom/vitest';
24
+ `;
25
+ const PACKAGE_JSON_SCRIPTS = {
26
+ ace: "tsx cli/index.ts",
27
+ test: "vitest run",
28
+ "test:watch": "vitest"
29
+ };
30
+ async function run(args) {
31
+ const { force, writeScripts } = parseArgs(args);
32
+ const root = process.cwd();
33
+ console.log(chalk.cyan("\n--- Initialize Workspace ---"));
34
+ console.log(chalk.dim(`Workspace: ${root}
35
+ `));
36
+ if (isWorkspaceInitialized(root) && !force) {
37
+ console.log(chalk.yellow("\u2713 Workspace already initialized (questions/ exists)"));
38
+ console.log(chalk.dim("Use --force to reinitialize.\n"));
39
+ return;
40
+ }
41
+ const changes = [];
42
+ const questionsDir = getQuestionsDir(root);
43
+ if (!fs.existsSync(questionsDir)) {
44
+ fs.mkdirSync(questionsDir, { recursive: true });
45
+ changes.push("Created questions/");
46
+ }
47
+ const vitestConfigPath = path.join(root, "vitest.config.ts");
48
+ if (!fs.existsSync(vitestConfigPath) || force) {
49
+ fs.writeFileSync(vitestConfigPath, VITEST_CONFIG_TEMPLATE, "utf-8");
50
+ changes.push(force && fs.existsSync(vitestConfigPath) ? "Overwrote vitest.config.ts" : "Created vitest.config.ts");
51
+ }
52
+ const vitestSetupPath = path.join(root, "vitest.setup.ts");
53
+ if (!fs.existsSync(vitestSetupPath) || force) {
54
+ fs.writeFileSync(vitestSetupPath, VITEST_SETUP_TEMPLATE, "utf-8");
55
+ changes.push(force && fs.existsSync(vitestSetupPath) ? "Overwrote vitest.setup.ts" : "Created vitest.setup.ts");
56
+ }
57
+ const packageJsonPath = path.join(root, "package.json");
58
+ if (fs.existsSync(packageJsonPath)) {
59
+ if (writeScripts) {
60
+ try {
61
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
62
+ pkg.scripts = pkg.scripts || {};
63
+ let added = false;
64
+ for (const [key, value] of Object.entries(PACKAGE_JSON_SCRIPTS)) {
65
+ if (!pkg.scripts[key]) {
66
+ pkg.scripts[key] = value;
67
+ added = true;
68
+ }
69
+ }
70
+ if (added) {
71
+ fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
72
+ changes.push("Added scripts to package.json");
73
+ }
74
+ } catch (err) {
75
+ console.warn(chalk.yellow("Warning: Could not update package.json scripts"));
76
+ }
77
+ }
78
+ }
79
+ if (changes.length > 0) {
80
+ console.log(chalk.green("\u2713 Workspace initialized:\n"));
81
+ for (const change of changes) {
82
+ console.log(chalk.dim(` \u2022 ${change}`));
83
+ }
84
+ } else {
85
+ console.log(chalk.green("\u2713 Workspace already initialized (no changes needed)"));
86
+ }
87
+ console.log();
88
+ console.log(chalk.bold("Next steps:"));
89
+ console.log(chalk.dim(" 1. Install dependencies (if not already installed):"));
90
+ console.log(chalk.dim(" npm install vitest happy-dom @testing-library/jest-dom"));
91
+ console.log(chalk.dim(" 2. Configure API keys:"));
92
+ console.log(chalk.dim(" ace setup"));
93
+ console.log(chalk.dim(" 3. Generate or add questions:"));
94
+ console.log(chalk.dim(' ace generate --topic "debounce"'));
95
+ console.log(chalk.dim(" ace add"));
96
+ console.log();
97
+ }
98
+ export {
99
+ run
100
+ };