create-interview-cockpit 0.1.1 → 0.3.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.
@@ -25,10 +25,11 @@ import { createAnthropic } from "@ai-sdk/anthropic";
25
25
  import { randomUUID } from "crypto";
26
26
  import fs from "fs/promises";
27
27
  import * as storage from "./storage.js";
28
+ import * as googleDrive from "./google-drive.js";
28
29
 
29
30
  const app = express();
30
31
  app.use(cors());
31
- app.use(express.json({ limit: "10mb" }));
32
+ app.use(express.json({ limit: "25mb" }));
32
33
 
33
34
  const upload = multer({
34
35
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
@@ -108,6 +109,191 @@ function getTextFromUIMessage(message: any): string {
108
109
  return "";
109
110
  }
110
111
 
112
+ // ─── Workspaces ──────────────────────────────────────────
113
+
114
+ app.get("/api/workspaces", async (_req, res) => {
115
+ const registry = await storage.getWorkspaces();
116
+ res.json(registry);
117
+ });
118
+
119
+ app.post("/api/workspaces", async (req, res) => {
120
+ const { name, type } = req.body;
121
+ if (!name || !type)
122
+ return res.status(400).json({ error: "name and type required" });
123
+ const ws: storage.WorkspaceMeta = {
124
+ id: randomUUID(),
125
+ name,
126
+ type: type === "google_drive" ? "google_drive" : "local",
127
+ createdAt: new Date().toISOString(),
128
+ };
129
+ await storage.saveWorkspace(ws);
130
+ const registry = await storage.getWorkspaces();
131
+ res.json(registry);
132
+ });
133
+
134
+ app.patch("/api/workspaces/:id", async (req, res) => {
135
+ const ws = await storage.updateWorkspace(req.params.id, req.body);
136
+ if (!ws) return res.status(404).json({ error: "Not found" });
137
+ const registry = await storage.getWorkspaces();
138
+ res.json(registry);
139
+ });
140
+
141
+ app.delete("/api/workspaces/:id", async (req, res) => {
142
+ try {
143
+ await storage.deleteWorkspace(req.params.id);
144
+ const registry = await storage.getWorkspaces();
145
+ res.json(registry);
146
+ } catch (err: any) {
147
+ res.status(400).json({ error: err?.message || "Failed to delete" });
148
+ }
149
+ });
150
+
151
+ app.post("/api/workspaces/:id/activate", async (req, res) => {
152
+ try {
153
+ const registry = await storage.activateWorkspace(req.params.id);
154
+ res.json(registry);
155
+ } catch (err: any) {
156
+ res.status(400).json({ error: err?.message || "Failed to activate" });
157
+ }
158
+ });
159
+
160
+ app.post("/api/workspaces/:id/sync", async (req, res) => {
161
+ try {
162
+ // Ensure all storage calls target this workspace (guards against server restarts
163
+ // that reset the in-memory _activeWorkspaceId).
164
+ storage.setActiveWorkspaceId(req.params.id);
165
+ const result = await googleDrive.syncWorkspace(req.params.id, extractText);
166
+ res.json(result);
167
+ } catch (err: any) {
168
+ console.error("[sync]", err);
169
+ res.status(500).json({ error: err?.message || "Sync failed" });
170
+ }
171
+ });
172
+
173
+ app.get("/api/workspaces/:id/drive-subfolders", async (req, res) => {
174
+ try {
175
+ const folders = await googleDrive.listDriveSubfolders(req.params.id);
176
+ res.json(folders);
177
+ } catch (err: any) {
178
+ res
179
+ .status(500)
180
+ .json({ error: err?.message || "Failed to list subfolders" });
181
+ }
182
+ });
183
+
184
+ app.post("/api/workspaces/:id/drive-subfolders", async (req, res) => {
185
+ const { name } = req.body as { name?: string };
186
+ if (!name?.trim()) return res.status(400).json({ error: "name required" });
187
+ try {
188
+ const folder = await googleDrive.createDriveSubfolder(
189
+ req.params.id,
190
+ name.trim(),
191
+ );
192
+ res.json(folder);
193
+ } catch (err: any) {
194
+ res.status(500).json({ error: err?.message || "Failed to create folder" });
195
+ }
196
+ });
197
+
198
+ app.get("/api/drive/export-auth", (_req, res) => {
199
+ res.redirect(googleDrive.getExportAuthUrl());
200
+ });
201
+
202
+ app.get("/api/drive/export-callback", async (req, res) => {
203
+ const code = req.query.code as string | undefined;
204
+ if (!code) return res.status(400).send("Missing code parameter");
205
+ try {
206
+ await googleDrive.handleExportCallback(code);
207
+ const clientUrl = process.env.CLIENT_URL ?? "http://localhost:5173";
208
+ res.redirect(`${clientUrl}/?export_authed=1`);
209
+ } catch (err: any) {
210
+ res
211
+ .status(500)
212
+ .send(`OAuth callback failed: ${err?.message || "unknown error"}`);
213
+ }
214
+ });
215
+
216
+ app.post("/api/workspaces/:id/export-drive", async (req, res) => {
217
+ try {
218
+ if (!(await googleDrive.isExportAuthed())) {
219
+ return res.json({
220
+ needsAuth: true,
221
+ authUrl: googleDrive.getExportAuthUrl(),
222
+ });
223
+ }
224
+ const { targetFolderId } = req.body as { targetFolderId?: string };
225
+ const result = await googleDrive.exportWorkspace(
226
+ req.params.id,
227
+ targetFolderId,
228
+ );
229
+ res.json(result);
230
+ } catch (err: any) {
231
+ console.error("[export-drive]", err);
232
+ res.status(500).json({ error: err?.message || "Export failed" });
233
+ }
234
+ });
235
+
236
+ app.post("/api/drive/repair-permissions", async (req, res) => {
237
+ try {
238
+ const { folderId } = req.body as { folderId: string };
239
+ if (!folderId) return res.status(400).json({ error: "folderId required" });
240
+ const fixed = await googleDrive.repairDriveFolderPermissions(folderId);
241
+ res.json({ fixed });
242
+ } catch (err: any) {
243
+ console.error("[repair-permissions]", err);
244
+ res.status(500).json({ error: err?.message || "Repair failed" });
245
+ }
246
+ });
247
+
248
+ app.post("/api/workspaces/:id/link-drive", async (req, res) => {
249
+ const { url } = req.body as { url?: string };
250
+ if (!url) return res.status(400).json({ error: "url required" });
251
+ const folderId = googleDrive.extractFolderIdFromUrl(url);
252
+ if (!folderId)
253
+ return res.status(400).json({
254
+ error:
255
+ "Could not extract folder ID from URL. Make sure it is a Google Drive folder share link.",
256
+ });
257
+ try {
258
+ const meta = await googleDrive.getFolderMeta(folderId);
259
+ await storage.updateWorkspace(req.params.id, {
260
+ driveConfig: { folderId: meta.id, folderName: meta.name },
261
+ });
262
+ // Return available folders so the client can let the user pick which to sync
263
+ const folders = await googleDrive.listDriveSubfolders(req.params.id);
264
+ const registry = await storage.getWorkspaces();
265
+ res.json({ registry, folders });
266
+ } catch (err: any) {
267
+ res
268
+ .status(500)
269
+ .json({ error: err?.message || "Failed to link Drive folder" });
270
+ }
271
+ });
272
+
273
+ /** Attach a Drive folder to any workspace type WITHOUT syncing (for local workspaces). */
274
+ app.post("/api/workspaces/:id/attach-drive", async (req, res) => {
275
+ const { url } = req.body as { url?: string };
276
+ if (!url) return res.status(400).json({ error: "url required" });
277
+ const folderId = googleDrive.extractFolderIdFromUrl(url);
278
+ if (!folderId)
279
+ return res.status(400).json({
280
+ error:
281
+ "Could not extract folder ID from URL. Make sure it is a Google Drive folder share link.",
282
+ });
283
+ try {
284
+ const meta = await googleDrive.getFolderMeta(folderId);
285
+ await storage.updateWorkspace(req.params.id, {
286
+ driveConfig: { folderId: meta.id, folderName: meta.name },
287
+ });
288
+ const registry = await storage.getWorkspaces();
289
+ res.json({ registry });
290
+ } catch (err: any) {
291
+ res
292
+ .status(500)
293
+ .json({ error: err?.message || "Failed to attach Drive folder" });
294
+ }
295
+ });
296
+
111
297
  // ─── Topics ──────────────────────────────────────────────
112
298
 
113
299
  app.get("/api/topics", async (_req, res) => {
@@ -753,6 +939,9 @@ app.post("/api/fix-diagram", async (req, res) => {
753
939
 
754
940
  // ─── Start ───────────────────────────────────────────────
755
941
 
756
- app.listen(PORT, () => {
757
- console.log(`\n ✈ Interview Cockpit server → http://localhost:${PORT}\n`);
758
- });
942
+ (async () => {
943
+ await storage.migrateToWorkspaces();
944
+ app.listen(PORT, () => {
945
+ console.log(`\n ✈ Interview Cockpit server → http://localhost:${PORT}\n`);
946
+ });
947
+ })();