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.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/commands/add.js +92 -0
- package/dist/commands/feedback.js +133 -0
- package/dist/commands/generate.js +224 -0
- package/dist/commands/init.js +100 -0
- package/dist/commands/list.js +107 -0
- package/dist/commands/reset.js +68 -0
- package/dist/commands/score.js +70 -0
- package/dist/commands/setup.js +84 -0
- package/dist/commands/test.js +85 -0
- package/dist/index.js +72 -0
- package/dist/lib/categories.js +103 -0
- package/dist/lib/config.js +61 -0
- package/dist/lib/llm.js +134 -0
- package/dist/lib/paths.js +38 -0
- package/dist/lib/scaffold.js +110 -0
- package/dist/lib/scorecard.js +116 -0
- package/dist/prompts/code-review.md +59 -0
- package/dist/prompts/design-review.md +67 -0
- package/dist/prompts/question-brainstorm.md +31 -0
- package/dist/prompts/question-generate.md +65 -0
- package/dist/templates/design/notes.md.hbs +27 -0
- package/dist/templates/js-ts/solution.test.ts.hbs +11 -0
- package/dist/templates/js-ts/solution.ts.hbs +11 -0
- package/dist/templates/leetcode-algo/solution.test.ts.hbs +11 -0
- package/dist/templates/leetcode-algo/solution.ts.hbs +11 -0
- package/dist/templates/leetcode-ds/solution.test.ts.hbs +11 -0
- package/dist/templates/leetcode-ds/solution.ts.hbs +11 -0
- package/dist/templates/react-apps/App.test.tsx.hbs +16 -0
- package/dist/templates/react-apps/App.tsx.hbs +16 -0
- package/dist/templates/readme.md.hbs +9 -0
- package/dist/templates/web-components/component.test.ts.hbs +11 -0
- package/dist/templates/web-components/component.ts.hbs +22 -0
- package/dist/templates/web-components/index.html.hbs +12 -0
- package/package.json +72 -0
- package/questions/design-be/url-shortener/README.md +23 -0
- package/questions/design-be/url-shortener/notes.md +27 -0
- package/questions/design-be/url-shortener/scorecard.json +1 -0
- package/questions/design-fe/news-feed/README.md +22 -0
- package/questions/design-fe/news-feed/notes.md +27 -0
- package/questions/design-fe/news-feed/scorecard.json +1 -0
- package/questions/design-full/google-docs/README.md +22 -0
- package/questions/design-full/google-docs/notes.md +27 -0
- package/questions/design-full/google-docs/scorecard.json +1 -0
- package/questions/js-ts/debounce/README.md +86 -0
- package/questions/js-ts/debounce/scorecard.json +9 -0
- package/questions/js-ts/debounce/solution.test.ts +128 -0
- package/questions/js-ts/debounce/solution.ts +4 -0
- package/questions/leetcode-algo/two-sum/README.md +58 -0
- package/questions/leetcode-algo/two-sum/scorecard.json +1 -0
- package/questions/leetcode-algo/two-sum/solution.test.ts +55 -0
- package/questions/leetcode-algo/two-sum/solution.ts +4 -0
- package/questions/leetcode-ds/lru-cache/README.md +70 -0
- package/questions/leetcode-ds/lru-cache/scorecard.json +1 -0
- package/questions/leetcode-ds/lru-cache/solution.test.ts +82 -0
- package/questions/leetcode-ds/lru-cache/solution.ts +14 -0
- package/questions/react-apps/todo-app/App.test.tsx +130 -0
- package/questions/react-apps/todo-app/App.tsx +10 -0
- package/questions/react-apps/todo-app/README.md +23 -0
- package/questions/react-apps/todo-app/scorecard.json +9 -0
- package/questions/web-components/star-rating/README.md +45 -0
- package/questions/web-components/star-rating/component.test.ts +64 -0
- package/questions/web-components/star-rating/component.ts +28 -0
- package/questions/web-components/star-rating/index.html +14 -0
- 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
|
+
};
|