ace-interview-prep 0.1.0 → 0.1.2
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/dist/commands/init.js +90 -37
- package/dist/commands/setup.js +42 -4
- package/dist/lib/llm.js +30 -1
- package/package.json +7 -2
package/dist/commands/init.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import { getQuestionsDir, isWorkspaceInitialized } from "../lib/paths.js";
|
|
5
6
|
function parseArgs(args) {
|
|
6
7
|
return {
|
|
7
|
-
force: args.includes("--force")
|
|
8
|
-
writeScripts: args.includes("--write-scripts")
|
|
8
|
+
force: args.includes("--force")
|
|
9
9
|
};
|
|
10
10
|
}
|
|
11
11
|
const VITEST_CONFIG_TEMPLATE = `import { defineConfig } from 'vitest/config';
|
|
@@ -22,13 +22,44 @@ export default defineConfig({
|
|
|
22
22
|
`;
|
|
23
23
|
const VITEST_SETUP_TEMPLATE = `import '@testing-library/jest-dom/vitest';
|
|
24
24
|
`;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const PACKAGE_JSON_TEMPLATE = {
|
|
26
|
+
name: "ace-workspace",
|
|
27
|
+
private: true,
|
|
28
|
+
type: "module",
|
|
29
|
+
scripts: {
|
|
30
|
+
test: "vitest run",
|
|
31
|
+
"test:watch": "vitest"
|
|
32
|
+
},
|
|
33
|
+
devDependencies: {
|
|
34
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
35
|
+
"@testing-library/react": "^16.3.2",
|
|
36
|
+
"@types/react": "^19.2.14",
|
|
37
|
+
"@types/react-dom": "^19.2.3",
|
|
38
|
+
"happy-dom": "^20.6.1",
|
|
39
|
+
"react": "^19.2.4",
|
|
40
|
+
"react-dom": "^19.2.4",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const TSCONFIG_TEMPLATE = {
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
target: "ES2022",
|
|
48
|
+
module: "ESNext",
|
|
49
|
+
moduleResolution: "bundler",
|
|
50
|
+
esModuleInterop: true,
|
|
51
|
+
allowImportingTsExtensions: true,
|
|
52
|
+
noEmit: true,
|
|
53
|
+
strict: true,
|
|
54
|
+
skipLibCheck: true,
|
|
55
|
+
resolveJsonModule: true,
|
|
56
|
+
isolatedModules: true,
|
|
57
|
+
jsx: "react-jsx"
|
|
58
|
+
},
|
|
59
|
+
include: ["questions/**/*"]
|
|
29
60
|
};
|
|
30
61
|
async function run(args) {
|
|
31
|
-
const { force
|
|
62
|
+
const { force } = parseArgs(args);
|
|
32
63
|
const root = process.cwd();
|
|
33
64
|
console.log(chalk.cyan("\n--- Initialize Workspace ---"));
|
|
34
65
|
console.log(chalk.dim(`Workspace: ${root}
|
|
@@ -44,38 +75,54 @@ async function run(args) {
|
|
|
44
75
|
fs.mkdirSync(questionsDir, { recursive: true });
|
|
45
76
|
changes.push("Created questions/");
|
|
46
77
|
}
|
|
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
78
|
const packageJsonPath = path.join(root, "package.json");
|
|
58
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
80
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(PACKAGE_JSON_TEMPLATE, null, 2) + "\n", "utf-8");
|
|
81
|
+
changes.push("Created package.json");
|
|
82
|
+
} else {
|
|
83
|
+
try {
|
|
84
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
85
|
+
let updated = false;
|
|
86
|
+
pkg.scripts = pkg.scripts || {};
|
|
87
|
+
for (const [key, value] of Object.entries(PACKAGE_JSON_TEMPLATE.scripts)) {
|
|
88
|
+
if (!pkg.scripts[key]) {
|
|
89
|
+
pkg.scripts[key] = value;
|
|
90
|
+
updated = true;
|
|
69
91
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
92
|
+
}
|
|
93
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
94
|
+
for (const [key, value] of Object.entries(PACKAGE_JSON_TEMPLATE.devDependencies)) {
|
|
95
|
+
if (!pkg.devDependencies[key]) {
|
|
96
|
+
pkg.devDependencies[key] = value;
|
|
97
|
+
updated = true;
|
|
73
98
|
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.warn(chalk.yellow("Warning: Could not update package.json scripts"));
|
|
76
99
|
}
|
|
100
|
+
if (updated) {
|
|
101
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
102
|
+
changes.push("Updated package.json (scripts and devDependencies)");
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.warn(chalk.yellow("Warning: Could not update package.json"));
|
|
77
106
|
}
|
|
78
107
|
}
|
|
108
|
+
const tsconfigPath = path.join(root, "tsconfig.json");
|
|
109
|
+
const tsconfigExisted = fs.existsSync(tsconfigPath);
|
|
110
|
+
if (!tsconfigExisted || force) {
|
|
111
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(TSCONFIG_TEMPLATE, null, 2) + "\n", "utf-8");
|
|
112
|
+
changes.push(tsconfigExisted ? "Overwrote tsconfig.json" : "Created tsconfig.json");
|
|
113
|
+
}
|
|
114
|
+
const vitestConfigPath = path.join(root, "vitest.config.ts");
|
|
115
|
+
const vitestConfigExisted = fs.existsSync(vitestConfigPath);
|
|
116
|
+
if (!vitestConfigExisted || force) {
|
|
117
|
+
fs.writeFileSync(vitestConfigPath, VITEST_CONFIG_TEMPLATE, "utf-8");
|
|
118
|
+
changes.push(vitestConfigExisted ? "Overwrote vitest.config.ts" : "Created vitest.config.ts");
|
|
119
|
+
}
|
|
120
|
+
const vitestSetupPath = path.join(root, "vitest.setup.ts");
|
|
121
|
+
const vitestSetupExisted = fs.existsSync(vitestSetupPath);
|
|
122
|
+
if (!vitestSetupExisted || force) {
|
|
123
|
+
fs.writeFileSync(vitestSetupPath, VITEST_SETUP_TEMPLATE, "utf-8");
|
|
124
|
+
changes.push(vitestSetupExisted ? "Overwrote vitest.setup.ts" : "Created vitest.setup.ts");
|
|
125
|
+
}
|
|
79
126
|
if (changes.length > 0) {
|
|
80
127
|
console.log(chalk.green("\u2713 Workspace initialized:\n"));
|
|
81
128
|
for (const change of changes) {
|
|
@@ -85,12 +132,18 @@ async function run(args) {
|
|
|
85
132
|
console.log(chalk.green("\u2713 Workspace already initialized (no changes needed)"));
|
|
86
133
|
}
|
|
87
134
|
console.log();
|
|
135
|
+
console.log(chalk.cyan("--- Installing dependencies ---"));
|
|
136
|
+
try {
|
|
137
|
+
execSync("npm install", { cwd: root, stdio: "inherit" });
|
|
138
|
+
console.log(chalk.green("\n\u2713 Dependencies installed"));
|
|
139
|
+
} catch {
|
|
140
|
+
console.error(chalk.red("\n\u2717 npm install failed. Please run it manually."));
|
|
141
|
+
}
|
|
142
|
+
console.log();
|
|
88
143
|
console.log(chalk.bold("Next steps:"));
|
|
89
|
-
console.log(chalk.dim(" 1.
|
|
90
|
-
console.log(chalk.dim(" npm install vitest happy-dom @testing-library/jest-dom"));
|
|
91
|
-
console.log(chalk.dim(" 2. Configure API keys:"));
|
|
144
|
+
console.log(chalk.dim(" 1. Configure API keys:"));
|
|
92
145
|
console.log(chalk.dim(" ace setup"));
|
|
93
|
-
console.log(chalk.dim("
|
|
146
|
+
console.log(chalk.dim(" 2. Generate or add questions:"));
|
|
94
147
|
console.log(chalk.dim(' ace generate --topic "debounce"'));
|
|
95
148
|
console.log(chalk.dim(" ace add"));
|
|
96
149
|
console.log();
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { saveGlobalAceConfig, maskApiKey, loadAceConfig } from "../lib/config.js";
|
|
4
|
+
import { validateOpenAIKey, validateAnthropicKey } from "../lib/llm.js";
|
|
4
5
|
function parseArgs(args) {
|
|
5
6
|
const result = {};
|
|
6
7
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -18,6 +19,11 @@ function parseArgs(args) {
|
|
|
18
19
|
}
|
|
19
20
|
return result;
|
|
20
21
|
}
|
|
22
|
+
function printStatusLine(label, status, detail) {
|
|
23
|
+
const icon = status === true ? chalk.green("\u2713") : status === false ? chalk.red("\u2717") : chalk.yellow("\u2013");
|
|
24
|
+
const paddedLabel = label.padEnd(18);
|
|
25
|
+
console.log(` ${icon} ${paddedLabel} ${chalk.dim(detail)}`);
|
|
26
|
+
}
|
|
21
27
|
async function run(args) {
|
|
22
28
|
const parsed = parseArgs(args);
|
|
23
29
|
console.log(chalk.cyan("\n--- Setup API Keys ---"));
|
|
@@ -69,15 +75,47 @@ async function run(args) {
|
|
|
69
75
|
} else {
|
|
70
76
|
console.log(chalk.yellow("\nNo new keys provided. Existing configuration unchanged."));
|
|
71
77
|
}
|
|
78
|
+
console.log(chalk.cyan("\n--- Validating API Keys ---\n"));
|
|
72
79
|
const final = loadAceConfig();
|
|
73
|
-
|
|
80
|
+
let openaiValid = null;
|
|
81
|
+
let openaiError;
|
|
82
|
+
if (final.OPENAI_API_KEY) {
|
|
83
|
+
console.log(chalk.dim("Validating OpenAI key..."));
|
|
84
|
+
const result = await validateOpenAIKey(final.OPENAI_API_KEY);
|
|
85
|
+
openaiValid = result.valid;
|
|
86
|
+
openaiError = result.error;
|
|
87
|
+
}
|
|
88
|
+
let anthropicValid = null;
|
|
89
|
+
let anthropicError;
|
|
90
|
+
if (final.ANTHROPIC_API_KEY) {
|
|
91
|
+
console.log(chalk.dim("Validating Anthropic key..."));
|
|
92
|
+
const result = await validateAnthropicKey(final.ANTHROPIC_API_KEY);
|
|
93
|
+
anthropicValid = result.valid;
|
|
94
|
+
anthropicError = result.error;
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.cyan("\n\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
97
|
+
console.log(chalk.cyan("\u2502") + chalk.bold(" ace status") + " " + chalk.cyan("\u2502"));
|
|
98
|
+
console.log(chalk.cyan("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
74
99
|
if (final.OPENAI_API_KEY) {
|
|
75
|
-
|
|
100
|
+
const detail = openaiValid ? maskApiKey(final.OPENAI_API_KEY) : openaiError || "validation failed";
|
|
101
|
+
printStatusLine("OpenAI key", openaiValid, detail);
|
|
102
|
+
} else {
|
|
103
|
+
printStatusLine("OpenAI key", null, "not configured");
|
|
76
104
|
}
|
|
77
105
|
if (final.ANTHROPIC_API_KEY) {
|
|
78
|
-
|
|
106
|
+
const detail = anthropicValid ? maskApiKey(final.ANTHROPIC_API_KEY) : anthropicError || "validation failed";
|
|
107
|
+
printStatusLine("Anthropic key", anthropicValid, detail);
|
|
108
|
+
} else {
|
|
109
|
+
printStatusLine("Anthropic key", null, "not configured");
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk.cyan("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
112
|
+
const ready = openaiValid === true || anthropicValid === true;
|
|
113
|
+
printStatusLine("Ready", ready, ready ? "at least one provider configured" : "no valid API keys");
|
|
114
|
+
console.log(chalk.cyan("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n"));
|
|
115
|
+
if (!ready && (openaiValid === false || anthropicValid === false)) {
|
|
116
|
+
console.log(chalk.yellow("Warning: No valid API keys configured."));
|
|
117
|
+
console.log(chalk.dim("Run `ace setup` again with valid keys to use LLM features.\n"));
|
|
79
118
|
}
|
|
80
|
-
console.log();
|
|
81
119
|
}
|
|
82
120
|
export {
|
|
83
121
|
run
|
package/dist/lib/llm.js
CHANGED
|
@@ -126,9 +126,38 @@ async function chatStream(provider, messages) {
|
|
|
126
126
|
}
|
|
127
127
|
return streamAnthropic(messages);
|
|
128
128
|
}
|
|
129
|
+
async function validateOpenAIKey(apiKey) {
|
|
130
|
+
try {
|
|
131
|
+
const client = new OpenAI({ apiKey });
|
|
132
|
+
await client.models.list();
|
|
133
|
+
return { valid: true };
|
|
134
|
+
} catch (err) {
|
|
135
|
+
const message = err?.message || "Unknown error";
|
|
136
|
+
return { valid: false, error: message };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function validateAnthropicKey(apiKey) {
|
|
140
|
+
try {
|
|
141
|
+
const client = new Anthropic({ apiKey });
|
|
142
|
+
await client.messages.create({
|
|
143
|
+
model: "claude-sonnet-4-20250514",
|
|
144
|
+
max_tokens: 1,
|
|
145
|
+
messages: [{ role: "user", content: "hi" }]
|
|
146
|
+
});
|
|
147
|
+
return { valid: true };
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err?.status === 401) {
|
|
150
|
+
return { valid: false, error: "Invalid API key (401 Unauthorized)" };
|
|
151
|
+
}
|
|
152
|
+
const message = err?.message || "Unknown error";
|
|
153
|
+
return { valid: false, error: message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
129
156
|
export {
|
|
130
157
|
chat,
|
|
131
158
|
chatStream,
|
|
132
159
|
getDefaultProvider,
|
|
133
|
-
requireProvider
|
|
160
|
+
requireProvider,
|
|
161
|
+
validateAnthropicKey,
|
|
162
|
+
validateOpenAIKey
|
|
134
163
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ace-interview-prep",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"prepublishOnly": "npm run build",
|
|
21
21
|
"ace": "tsx cli/index.ts",
|
|
22
22
|
"test": "vitest run",
|
|
23
|
-
"test:watch": "vitest"
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"prepare": "husky",
|
|
25
|
+
"release": "npx commit-and-tag-version"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
26
28
|
"interview",
|
|
@@ -56,12 +58,15 @@
|
|
|
56
58
|
"prompts": "^2.4.2"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
61
|
+
"@commitlint/cli": "^20.4.1",
|
|
62
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
59
63
|
"@testing-library/jest-dom": "^6.9.1",
|
|
60
64
|
"@testing-library/react": "^16.3.2",
|
|
61
65
|
"@types/prompts": "^2.4.9",
|
|
62
66
|
"@types/react": "^19.2.14",
|
|
63
67
|
"@types/react-dom": "^19.2.3",
|
|
64
68
|
"happy-dom": "^20.6.1",
|
|
69
|
+
"husky": "^9.1.7",
|
|
65
70
|
"react": "^19.2.4",
|
|
66
71
|
"react-dom": "^19.2.4",
|
|
67
72
|
"tsup": "^8.5.1",
|