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.
@@ -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 PACKAGE_JSON_SCRIPTS = {
26
- ace: "tsx cli/index.ts",
27
- test: "vitest run",
28
- "test:watch": "vitest"
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, writeScripts } = parseArgs(args);
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
- 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
- }
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
- if (added) {
71
- fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
72
- changes.push("Added scripts to package.json");
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. 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:"));
144
+ console.log(chalk.dim(" 1. Configure API keys:"));
92
145
  console.log(chalk.dim(" ace setup"));
93
- console.log(chalk.dim(" 3. Generate or add questions:"));
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();
@@ -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
- console.log(chalk.dim("\nConfigured providers:"));
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
- console.log(chalk.dim(` \u2022 OpenAI: ${maskApiKey(final.OPENAI_API_KEY)}`));
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
- console.log(chalk.dim(` \u2022 Anthropic: ${maskApiKey(final.ANTHROPIC_API_KEY)}`));
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.0",
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",