create-interview-cockpit 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 (39) hide show
  1. package/README.md +62 -0
  2. package/index.js +302 -0
  3. package/package.json +44 -0
  4. package/template/.env.example +14 -0
  5. package/template/client/index.html +12 -0
  6. package/template/client/package-lock.json +6012 -0
  7. package/template/client/package.json +34 -0
  8. package/template/client/postcss.config.cjs +6 -0
  9. package/template/client/src/App.tsx +120 -0
  10. package/template/client/src/api.ts +132 -0
  11. package/template/client/src/components/AnnotationDialog.tsx +307 -0
  12. package/template/client/src/components/ChatMessage.tsx +89 -0
  13. package/template/client/src/components/ChatView.tsx +763 -0
  14. package/template/client/src/components/CodeContextPanel.tsx +470 -0
  15. package/template/client/src/components/FileAttachments.tsx +107 -0
  16. package/template/client/src/components/FileViewerModal.tsx +470 -0
  17. package/template/client/src/components/MarkdownRenderer.tsx +333 -0
  18. package/template/client/src/components/MermaidDiagram.tsx +157 -0
  19. package/template/client/src/components/Sidebar.tsx +419 -0
  20. package/template/client/src/components/TextAnnotator.tsx +476 -0
  21. package/template/client/src/index.css +61 -0
  22. package/template/client/src/main.tsx +10 -0
  23. package/template/client/src/store.ts +321 -0
  24. package/template/client/src/types.ts +65 -0
  25. package/template/client/src/vite-env.d.ts +1 -0
  26. package/template/client/tailwind.config.cjs +8 -0
  27. package/template/client/tsconfig.json +16 -0
  28. package/template/client/tsconfig.tsbuildinfo +1 -0
  29. package/template/client/vite.config.ts +12 -0
  30. package/template/cockpit.json +3 -0
  31. package/template/data/context-files/.gitkeep +0 -0
  32. package/template/data/questions/.gitkeep +0 -0
  33. package/template/data/topics.json +1 -0
  34. package/template/package.json +14 -0
  35. package/template/server/package-lock.json +2266 -0
  36. package/template/server/package.json +31 -0
  37. package/template/server/src/index.ts +758 -0
  38. package/template/server/src/storage.ts +303 -0
  39. package/template/server/tsconfig.json +14 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # create-interview-cockpit
2
+
3
+ Scaffold your own AI-powered interview prep cockpit in seconds.
4
+
5
+ ```bash
6
+ npx create-interview-cockpit
7
+ ```
8
+
9
+ ## What you get
10
+
11
+ A self-hosted, full-stack interview prep tool with:
12
+
13
+ - **Topics** — organise prep by theme (C#, System Design, Azure, etc.)
14
+ - **Questions** — nested questions under each topic with persistent chat history
15
+ - **AI chat** — powered by any of OpenAI / Google Gemini / Anthropic Claude via the Vercel AI SDK
16
+ - **Context files** — attach PDFs, DOCX, or plain text (job specs, CVs, notes) as context per topic or question
17
+ - **Code context panel** — point the AI at your local codebase files
18
+ - **Text annotations** — highlight any AI response and ask follow-up questions inline
19
+ - **Response tuning** — control length (concise / moderate / full) and style (prose / bullets / structured)
20
+ - **Mermaid diagrams** — the AI renders architecture diagrams inline
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ npx create-interview-cockpit my-cockpit
26
+ cd my-cockpit
27
+ npm install
28
+ npm run dev
29
+ ```
30
+
31
+ The CLI will prompt you to choose an AI provider (OpenAI, Google, or Anthropic) and optionally paste your API key. Everything is written to a local `.env` file — nothing leaves your machine.
32
+
33
+ Open **http://localhost:5173**.
34
+
35
+ ## Tech stack
36
+
37
+ | Layer | Tech |
38
+ |----------|--------------------------------------|
39
+ | Frontend | React 19, TypeScript, Vite, Tailwind |
40
+ | Backend | Express, TypeScript, tsx |
41
+ | AI | Vercel AI SDK (multi-provider) |
42
+ | Storage | Local JSON files |
43
+
44
+ ## Supported AI providers
45
+
46
+ | Provider | Env var | Default model |
47
+ |-----------|----------------------|----------------------------|
48
+ | OpenAI | `OPENAI_API_KEY` | `gpt-4o` |
49
+ | Google | `GOOGLE_API_KEY` | `gemini-2.5-flash` |
50
+ | Anthropic | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
51
+
52
+ Switch provider at any time by editing `.env`:
53
+
54
+ ```ini
55
+ AI_PROVIDER=google
56
+ AI_MODEL=gemini-2.5-flash
57
+ GOOGLE_API_KEY=your-key-here
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
package/index.js ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import readline from "readline";
6
+ import { fileURLToPath } from "url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const templateDir = path.join(__dirname, "template");
11
+
12
+ // This CLI's own version — used for upgrade version stamping
13
+ const CLI_VERSION = JSON.parse(
14
+ fs.readFileSync(path.join(__dirname, "package.json"), "utf-8")
15
+ ).version;
16
+
17
+ // ── Readline helpers ───────────────────────────────────────
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+
23
+ const ask = (question, fallback) =>
24
+ new Promise((resolve) => {
25
+ const suffix = fallback ? ` (${fallback}): ` : ": ";
26
+ rl.question(question + suffix, (answer) => {
27
+ resolve(answer.trim() || fallback || "");
28
+ });
29
+ });
30
+
31
+ const askChoice = (question, choices, fallback) =>
32
+ new Promise((resolve) => {
33
+ rl.question(`${question} [${choices.join("/")}] (${fallback}): `, (answer) => {
34
+ const v = answer.trim().toLowerCase();
35
+ resolve(choices.includes(v) ? v : fallback);
36
+ });
37
+ });
38
+
39
+ const confirm = (message) =>
40
+ new Promise((resolve) => {
41
+ rl.question(`${message} [y/N]: `, (answer) => {
42
+ resolve(answer.trim().toLowerCase() === "y");
43
+ });
44
+ });
45
+
46
+ // ── Helpers ────────────────────────────────────────────────
47
+ // Files/dirs never overwritten during upgrade
48
+ const UPGRADE_KEEP = new Set([".env", "data", "node_modules", "dist"]);
49
+ // Files/dirs excluded when copying from the template
50
+ const COPY_EXCLUDE = new Set(["node_modules", "dist", ".env"]);
51
+
52
+ function copyDirSync(src, dest, exclude = COPY_EXCLUDE) {
53
+ fs.mkdirSync(dest, { recursive: true });
54
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
55
+ if (exclude.has(entry.name)) continue;
56
+ const srcPath = path.join(src, entry.name);
57
+ const destPath = path.join(dest, entry.name);
58
+ if (entry.isDirectory()) {
59
+ copyDirSync(srcPath, destPath, exclude);
60
+ } else {
61
+ fs.copyFileSync(srcPath, destPath);
62
+ }
63
+ }
64
+ }
65
+
66
+ /** Merge incoming deps into existing — never downgrade a pinned version. */
67
+ function mergeDeps(existing = {}, incoming = {}) {
68
+ const merged = { ...existing };
69
+ for (const [pkg, ver] of Object.entries(incoming)) {
70
+ if (!(pkg in merged)) merged[pkg] = ver;
71
+ }
72
+ return merged;
73
+ }
74
+
75
+ // ── Create command ────────────────────────────────────────
76
+ async function runCreate() {
77
+ const projectArg = process.argv[2];
78
+
79
+ console.log("");
80
+ console.log("╔══════════════════════════════════════════════════╗");
81
+ console.log("║ create-interview-cockpit ║");
82
+ console.log("║ Your personal AI-powered interview prep tool ║");
83
+ console.log("╚══════════════════════════════════════════════════╝");
84
+ console.log("");
85
+
86
+ // ── Project directory ──────────────────────────────────
87
+ const projectName =
88
+ projectArg || (await ask(" Project directory name", "my-cockpit"));
89
+ const targetDir = path.resolve(process.cwd(), projectName);
90
+
91
+ if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
92
+ console.error(`\n ✖ Directory "${projectName}" already exists and is not empty.\n`);
93
+ rl.close();
94
+ process.exit(1);
95
+ }
96
+
97
+ // ── AI Provider ────────────────────────────────────────
98
+ console.log("");
99
+ console.log(" Interview Cockpit supports OpenAI, Google Gemini, and Anthropic Claude.");
100
+ console.log("");
101
+ const provider = await askChoice(
102
+ " AI provider",
103
+ ["openai", "google", "anthropic"],
104
+ "openai"
105
+ );
106
+
107
+ const keyEnvNames = {
108
+ openai: "OPENAI_API_KEY",
109
+ google: "GOOGLE_API_KEY",
110
+ anthropic: "ANTHROPIC_API_KEY",
111
+ };
112
+ const defaultModels = {
113
+ openai: "gpt-4o",
114
+ google: "gemini-2.5-flash",
115
+ anthropic: "claude-sonnet-4-20250514",
116
+ };
117
+ const keyName = keyEnvNames[provider];
118
+ const defaultModel = defaultModels[provider];
119
+
120
+ const apiKey = await ask(
121
+ ` ${keyName} (paste your key, or leave blank to add to .env later)`,
122
+ ""
123
+ );
124
+ const model = await ask(" Model name", defaultModel);
125
+
126
+ console.log("");
127
+ console.log(" ─── Summary ─────────────────────────────────────");
128
+ console.log(" Directory : " + projectName);
129
+ console.log(" Provider : " + provider);
130
+ console.log(" Model : " + model);
131
+ console.log(" ─────────────────────────────────────────────────");
132
+ console.log("");
133
+
134
+ // ── 1. Copy template ──────────────────────────────────
135
+ console.log(" Scaffolding project…");
136
+ copyDirSync(templateDir, targetDir);
137
+ // cockpit.json is already in the template with the correct version
138
+ console.log(" ✔ Copied template");
139
+
140
+ // ── 2. Write .env ─────────────────────────────────────
141
+ const envLines = [
142
+ `# AI provider: openai | google | anthropic`,
143
+ `AI_PROVIDER=${provider}`,
144
+ `AI_MODEL=${model}`,
145
+ ``,
146
+ `# Paste your API key for the provider you selected`,
147
+ `OPENAI_API_KEY=${provider === "openai" ? apiKey : ""}`,
148
+ `GOOGLE_API_KEY=${provider === "google" ? apiKey : ""}`,
149
+ `ANTHROPIC_API_KEY=${provider === "anthropic" ? apiKey : ""}`,
150
+ ``,
151
+ `# Port the backend runs on`,
152
+ `PORT=3001`,
153
+ ``,
154
+ `# Optional: absolute path to a local code directory you want the AI to reference`,
155
+ `# CODE_CONTEXT_DIR=`,
156
+ ];
157
+ fs.writeFileSync(path.join(targetDir, ".env"), envLines.join("\n") + "\n");
158
+ console.log(" ✔ Created .env");
159
+
160
+ rl.close();
161
+
162
+ console.log("");
163
+ console.log(" ✔ Done! Get started:");
164
+ console.log("");
165
+ console.log(` cd ${projectName}`);
166
+ console.log(" npm install");
167
+ console.log(" npm run dev");
168
+ console.log("");
169
+ console.log(" The app opens at http://localhost:5173");
170
+ console.log(" Add topics, create questions, and start prepping.");
171
+ console.log("");
172
+ }
173
+
174
+ // ── Upgrade command ────────────────────────────────────────
175
+ async function runUpgrade() {
176
+ const cwd = process.cwd();
177
+
178
+ console.log("");
179
+ console.log("╔══════════════════════════════════════════════════╗");
180
+ console.log("║ create-interview-cockpit — upgrade ║");
181
+ console.log("╚══════════════════════════════════════════════════╝");
182
+ console.log("");
183
+
184
+ // ── Detect cockpit project ─────────────────────────────
185
+ const hasClient = fs.existsSync(path.join(cwd, "client"));
186
+ const hasServer = fs.existsSync(path.join(cwd, "server"));
187
+
188
+ if (!hasClient || !hasServer) {
189
+ console.error(
190
+ " ✖ This doesn't look like an Interview Cockpit project.\n" +
191
+ " Run this command from inside a cockpit project directory.\n"
192
+ );
193
+ rl.close();
194
+ process.exit(1);
195
+ }
196
+
197
+ // ── Version comparison ─────────────────────────────────
198
+ const cockpitJsonPath = path.join(cwd, "cockpit.json");
199
+ let currentVersion = "unknown";
200
+ if (fs.existsSync(cockpitJsonPath)) {
201
+ try {
202
+ currentVersion = JSON.parse(
203
+ fs.readFileSync(cockpitJsonPath, "utf-8")
204
+ ).version;
205
+ } catch {
206
+ // malformed cockpit.json
207
+ }
208
+ }
209
+
210
+ const currentLabel =
211
+ currentVersion === "unknown" ? "(pre-versioning / unknown)" : `v${currentVersion}`;
212
+
213
+ console.log(` Installed version : ${currentLabel}`);
214
+ console.log(` Available version : v${CLI_VERSION}`);
215
+ console.log("");
216
+
217
+ if (currentVersion === CLI_VERSION) {
218
+ console.log(" Already up to date. Nothing to do.");
219
+ console.log("");
220
+ rl.close();
221
+ return;
222
+ }
223
+
224
+ // ── What will change ───────────────────────────────────
225
+ console.log(" The following will be updated:");
226
+ console.log(" ✦ client/ (all source files)");
227
+ console.log(" ✦ server/ (all source files)");
228
+ console.log(" ✦ package.json (new deps merged in, your pinned versions preserved)");
229
+ console.log("");
230
+ console.log(" These will NOT be touched:");
231
+ console.log(" ✧ .env");
232
+ console.log(" ✧ data/ (your topics, questions, and context files)");
233
+ console.log(" ✧ node_modules/");
234
+ console.log("");
235
+
236
+ const ok = await confirm(
237
+ ` Upgrade ${currentLabel} → v${CLI_VERSION}?`
238
+ );
239
+ if (!ok) {
240
+ console.log("\n Upgrade cancelled.\n");
241
+ rl.close();
242
+ return;
243
+ }
244
+
245
+ console.log("");
246
+
247
+ // ── Apply upgrade ──────────────────────────────────────
248
+ copyDirSync(path.join(templateDir, "client"), path.join(cwd, "client"));
249
+ console.log(" ✔ client/");
250
+
251
+ copyDirSync(path.join(templateDir, "server"), path.join(cwd, "server"));
252
+ console.log(" ✔ server/");
253
+
254
+ // package.json — merge scripts + deps, never replace outright
255
+ const templatePkgPath = path.join(templateDir, "package.json");
256
+ const targetPkgPath = path.join(cwd, "package.json");
257
+ if (fs.existsSync(templatePkgPath) && fs.existsSync(targetPkgPath)) {
258
+ const tmpl = JSON.parse(fs.readFileSync(templatePkgPath, "utf-8"));
259
+ const target = JSON.parse(fs.readFileSync(targetPkgPath, "utf-8"));
260
+ // Template scripts win (may contain new like sync:template)
261
+ target.scripts = { ...target.scripts, ...tmpl.scripts };
262
+ // Deps — only add new packages
263
+ target.dependencies = mergeDeps(target.dependencies, tmpl.dependencies);
264
+ target.devDependencies = mergeDeps(
265
+ target.devDependencies,
266
+ tmpl.devDependencies
267
+ );
268
+ fs.writeFileSync(targetPkgPath, JSON.stringify(target, null, 2) + "\n");
269
+ console.log(" ✔ package.json (deps merged)");
270
+ }
271
+
272
+ // Stamp the new version
273
+ fs.writeFileSync(
274
+ cockpitJsonPath,
275
+ JSON.stringify({ version: CLI_VERSION }, null, 2) + "\n"
276
+ );
277
+ console.log(` ✔ cockpit.json → v${CLI_VERSION}`);
278
+
279
+ rl.close();
280
+
281
+ console.log("");
282
+ console.log(` ✔ Upgraded to v${CLI_VERSION}!`);
283
+ console.log("");
284
+ console.log(" Run npm install to pick up any dependency changes,");
285
+ console.log(" then npm run dev to start.");
286
+ console.log("");
287
+ }
288
+
289
+ // ── Entry point ────────────────────────────────────────────
290
+ const subcommand = process.argv[2];
291
+
292
+ if (subcommand === "upgrade") {
293
+ runUpgrade().catch((e) => {
294
+ console.error(e);
295
+ process.exit(1);
296
+ });
297
+ } else {
298
+ runCreate().catch((e) => {
299
+ console.error(e);
300
+ process.exit(1);
301
+ });
302
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "create-interview-cockpit",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a personal AI-powered interview prep cockpit",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-interview-cockpit": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "template"
12
+ ],
13
+ "scripts": {
14
+ "changeset": "changeset"
15
+ },
16
+ "keywords": [
17
+ "interview",
18
+ "ai",
19
+ "cockpit",
20
+ "scaffold",
21
+ "create",
22
+ "prep"
23
+ ],
24
+ "author": {
25
+ "name": "Chipili Kafwilo",
26
+ "email": "ckafwilo@gmail.com",
27
+ "url": "https://chipilidev.com"
28
+ },
29
+ "license": "MIT",
30
+ "devDependencies": {
31
+ "@changesets/changelog-github": "^0.5.2",
32
+ "@changesets/cli": "^2.29.6"
33
+ },
34
+ "engines": {
35
+ "node": ">=20"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/ChipiKaf/create-interview-cockpit.git"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
@@ -0,0 +1,14 @@
1
+ # AI provider: openai | google | anthropic
2
+ AI_PROVIDER=openai
3
+ AI_MODEL=gpt-4o
4
+
5
+ # Paste the key for whichever provider you use
6
+ OPENAI_API_KEY=
7
+ GOOGLE_API_KEY=
8
+ ANTHROPIC_API_KEY=
9
+
10
+ # Port the backend listens on
11
+ PORT=3001
12
+
13
+ # Optional: absolute path to a local code directory you want the AI to reference
14
+ # CODE_CONTEXT_DIR=
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Interview Cockpit</title>
7
+ </head>
8
+ <body class="bg-slate-950 text-slate-100">
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>