create-interview-cockpit 0.17.2 → 0.17.3
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/package.json
CHANGED
|
@@ -137,6 +137,86 @@ async function downloadFileAuthed(
|
|
|
137
137
|
return Buffer.from(res.data as ArrayBuffer);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
const WORKSPACE_FILES_FOLDER_NAME = "workspace-files";
|
|
141
|
+
const RESERVED_EXPORT_FOLDER_NAMES = new Set([WORKSPACE_FILES_FOLDER_NAME]);
|
|
142
|
+
|
|
143
|
+
function extensionForFilename(filename: string): string {
|
|
144
|
+
return filename.split(".").pop()?.toLowerCase() ?? "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function mimeForFilename(filename: string): string {
|
|
148
|
+
const ext = extensionForFilename(filename);
|
|
149
|
+
const map: Record<string, string> = {
|
|
150
|
+
pdf: "application/pdf",
|
|
151
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
152
|
+
doc: "application/msword",
|
|
153
|
+
json: "application/json",
|
|
154
|
+
csv: "text/csv",
|
|
155
|
+
html: "text/html; charset=utf-8",
|
|
156
|
+
xml: "application/xml",
|
|
157
|
+
txt: "text/plain; charset=utf-8",
|
|
158
|
+
md: "text/markdown; charset=utf-8",
|
|
159
|
+
png: "image/png",
|
|
160
|
+
jpg: "image/jpeg",
|
|
161
|
+
jpeg: "image/jpeg",
|
|
162
|
+
gif: "image/gif",
|
|
163
|
+
webp: "image/webp",
|
|
164
|
+
svg: "image/svg+xml",
|
|
165
|
+
};
|
|
166
|
+
return map[ext] ?? "application/octet-stream";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type SerializedContextFile = storage.ContextFile & {
|
|
170
|
+
content?: string;
|
|
171
|
+
originalBase64?: string;
|
|
172
|
+
originalMimeType?: string;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
async function readContextFileExportBytes(
|
|
176
|
+
cf: storage.ContextFile,
|
|
177
|
+
workspaceId: string,
|
|
178
|
+
ctxDir: string,
|
|
179
|
+
): Promise<Buffer> {
|
|
180
|
+
const original = await storage.readOriginalBlob(cf.id, workspaceId);
|
|
181
|
+
return original ?? fs.readFile(path.join(ctxDir, cf.id));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function serializeContextFileForExport(
|
|
185
|
+
cf: storage.ContextFile,
|
|
186
|
+
workspaceId: string,
|
|
187
|
+
ctxDir: string,
|
|
188
|
+
): Promise<SerializedContextFile> {
|
|
189
|
+
const exported: SerializedContextFile = { ...cf };
|
|
190
|
+
try {
|
|
191
|
+
exported.content = await fs.readFile(path.join(ctxDir, cf.id), "utf-8");
|
|
192
|
+
} catch {
|
|
193
|
+
/* Keep metadata even if the extracted-text blob is missing. */
|
|
194
|
+
}
|
|
195
|
+
const original = await storage.readOriginalBlob(cf.id, workspaceId);
|
|
196
|
+
if (original) {
|
|
197
|
+
exported.originalBase64 = original.toString("base64");
|
|
198
|
+
exported.originalMimeType = mimeForFilename(cf.originalName);
|
|
199
|
+
}
|
|
200
|
+
return exported;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function contextFileMetadataForImport(
|
|
204
|
+
cf: SerializedContextFile,
|
|
205
|
+
fallbackName: string,
|
|
206
|
+
): storage.ContextFile {
|
|
207
|
+
return {
|
|
208
|
+
id: cf.id,
|
|
209
|
+
name: cf.name || fallbackName,
|
|
210
|
+
originalName: cf.originalName || fallbackName,
|
|
211
|
+
...(cf.driveFileId ? { driveFileId: cf.driveFileId } : {}),
|
|
212
|
+
createdAt: cf.createdAt || new Date().toISOString(),
|
|
213
|
+
...(cf.origin ? { origin: cf.origin } : {}),
|
|
214
|
+
...(cf.language ? { language: cf.language } : {}),
|
|
215
|
+
...(cf.label ? { label: cf.label } : {}),
|
|
216
|
+
...(typeof cf.inContext === "boolean" ? { inContext: cf.inContext } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
140
220
|
export interface SyncResult {
|
|
141
221
|
topicsUpserted: number;
|
|
142
222
|
filesImported: number;
|
|
@@ -188,25 +268,38 @@ export async function syncWorkspace(
|
|
|
188
268
|
|
|
189
269
|
// ── Phase 1: fetch all folder listings in parallel ──────────────────────────
|
|
190
270
|
const subfolders = await doListFolders(syncRoot);
|
|
271
|
+
const workspaceFilesFolder = subfolders.find(
|
|
272
|
+
(folder) => folder.name === WORKSPACE_FILES_FOLDER_NAME,
|
|
273
|
+
);
|
|
274
|
+
const topicFolders = subfolders.filter(
|
|
275
|
+
(folder) => !RESERVED_EXPORT_FOLDER_NAMES.has(folder.name),
|
|
276
|
+
);
|
|
191
277
|
|
|
192
|
-
const folderData = await Promise.all(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
278
|
+
const [folderData, workspaceFileItems] = await Promise.all([
|
|
279
|
+
Promise.all(
|
|
280
|
+
topicFolders.map(async (folder) => {
|
|
281
|
+
const innerFolders = await doListFolders(folder.id);
|
|
282
|
+
const questionsFolder = innerFolders.find(
|
|
283
|
+
(f) => f.name === "questions",
|
|
284
|
+
);
|
|
285
|
+
const ctxFilesFolder = innerFolders.find(
|
|
286
|
+
(f) => f.name === "context-files",
|
|
287
|
+
);
|
|
199
288
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
289
|
+
const [qFiles, cfFiles] = await Promise.all([
|
|
290
|
+
questionsFolder
|
|
291
|
+
? doListFiles(questionsFolder.id)
|
|
292
|
+
: doListFiles(folder.id),
|
|
293
|
+
ctxFilesFolder ? doListFiles(ctxFilesFolder.id) : Promise.resolve([]),
|
|
294
|
+
]);
|
|
206
295
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
296
|
+
return { folder, qFiles, cfFiles };
|
|
297
|
+
}),
|
|
298
|
+
),
|
|
299
|
+
workspaceFilesFolder
|
|
300
|
+
? doListFiles(workspaceFilesFolder.id)
|
|
301
|
+
: Promise.resolve([]),
|
|
302
|
+
]);
|
|
210
303
|
|
|
211
304
|
// ── Phase 2: write all topics in one batch (one topics.json write) ───────────
|
|
212
305
|
const topicRecords: storage.Topic[] = folderData.map(({ folder }) => ({
|
|
@@ -229,8 +322,13 @@ export async function syncWorkspace(
|
|
|
229
322
|
buffer: Buffer;
|
|
230
323
|
isContextFile: boolean;
|
|
231
324
|
};
|
|
325
|
+
type PendingWorkspaceFile = {
|
|
326
|
+
filename: string;
|
|
327
|
+
buffer: Buffer;
|
|
328
|
+
};
|
|
232
329
|
|
|
233
330
|
const pending: Pending[] = [];
|
|
331
|
+
const pendingWorkspaceFiles: PendingWorkspaceFile[] = [];
|
|
234
332
|
|
|
235
333
|
await Promise.all(
|
|
236
334
|
folderData.flatMap(({ folder, qFiles, cfFiles }) => {
|
|
@@ -274,6 +372,19 @@ export async function syncWorkspace(
|
|
|
274
372
|
}),
|
|
275
373
|
);
|
|
276
374
|
|
|
375
|
+
await Promise.all(
|
|
376
|
+
workspaceFileItems.map(async (file) => {
|
|
377
|
+
try {
|
|
378
|
+
const buffer = await doDownload(file.id, file.mimeType);
|
|
379
|
+
pendingWorkspaceFiles.push({ filename: file.name, buffer });
|
|
380
|
+
} catch (err: any) {
|
|
381
|
+
result.errors.push(
|
|
382
|
+
`Workspace file "${file.name}": ${err?.message ?? "failed"}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
);
|
|
387
|
+
|
|
277
388
|
// Root-level files in syncRoot → "General" topic
|
|
278
389
|
const rootFiles = await doListFiles(syncRoot);
|
|
279
390
|
if (rootFiles.length > 0) {
|
|
@@ -345,42 +456,53 @@ export async function syncWorkspace(
|
|
|
345
456
|
parentTitle = parsed.parentTitle || null;
|
|
346
457
|
if (parsed.createdAt) restoredCreatedAt = parsed.createdAt;
|
|
347
458
|
|
|
348
|
-
// Restore
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
parsed.
|
|
352
|
-
)
|
|
459
|
+
// Restore all question-attached context files. New exports use
|
|
460
|
+
// contextFiles; older exports used codeSnippets for lab/code blobs.
|
|
461
|
+
const parsedContextFiles: SerializedContextFile[] = Array.isArray(
|
|
462
|
+
parsed.contextFiles,
|
|
463
|
+
)
|
|
464
|
+
? parsed.contextFiles
|
|
465
|
+
: Array.isArray(parsed.codeSnippets)
|
|
466
|
+
? parsed.codeSnippets
|
|
467
|
+
: [];
|
|
468
|
+
if (parsedContextFiles.length > 0) {
|
|
353
469
|
const ctxDir =
|
|
354
470
|
storage.getContextFilesDirForWorkspace(workspaceId);
|
|
355
471
|
await Promise.all(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
cs.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
472
|
+
parsedContextFiles.map(async (cs) => {
|
|
473
|
+
if (!cs.id) return;
|
|
474
|
+
const originalName = cs.originalName || cs.name || cs.id;
|
|
475
|
+
try {
|
|
476
|
+
const originalBuffer = cs.originalBase64
|
|
477
|
+
? Buffer.from(cs.originalBase64, "base64")
|
|
478
|
+
: null;
|
|
479
|
+
const content =
|
|
480
|
+
typeof cs.content === "string"
|
|
481
|
+
? cs.content
|
|
482
|
+
: originalBuffer
|
|
483
|
+
? await extractText(originalBuffer, originalName)
|
|
484
|
+
: null;
|
|
485
|
+
|
|
486
|
+
if (content == null) return;
|
|
487
|
+
await fs.mkdir(ctxDir, { recursive: true });
|
|
488
|
+
await fs.writeFile(
|
|
489
|
+
path.join(ctxDir, cs.id),
|
|
490
|
+
Buffer.from(content, "utf-8"),
|
|
491
|
+
);
|
|
492
|
+
if (originalBuffer) {
|
|
493
|
+
await storage.writeOriginalBlob(
|
|
494
|
+
cs.id,
|
|
495
|
+
originalBuffer,
|
|
496
|
+
workspaceId,
|
|
497
|
+
);
|
|
381
498
|
}
|
|
382
|
-
|
|
383
|
-
|
|
499
|
+
restoredContextFiles.push(
|
|
500
|
+
contextFileMetadataForImport(cs, originalName),
|
|
501
|
+
);
|
|
502
|
+
} catch {
|
|
503
|
+
/* skip bad blobs */
|
|
504
|
+
}
|
|
505
|
+
}),
|
|
384
506
|
);
|
|
385
507
|
}
|
|
386
508
|
} catch {
|
|
@@ -439,13 +561,36 @@ export async function syncWorkspace(
|
|
|
439
561
|
}
|
|
440
562
|
}
|
|
441
563
|
|
|
442
|
-
// ── Phase 5: save context file blobs
|
|
564
|
+
// ── Phase 5: save workspace/topic context file blobs ───────────────────────
|
|
565
|
+
if (pendingWorkspaceFiles.length > 0) {
|
|
566
|
+
await Promise.all(
|
|
567
|
+
pendingWorkspaceFiles.map(async ({ filename, buffer }) => {
|
|
568
|
+
const fileId = randomUUID();
|
|
569
|
+
const text = await extractText(buffer, filename);
|
|
570
|
+
await storage.writeOriginalBlob(fileId, buffer, workspaceId);
|
|
571
|
+
await storage.saveWorkspaceContextFile(
|
|
572
|
+
fileId,
|
|
573
|
+
filename,
|
|
574
|
+
Buffer.from(text, "utf-8"),
|
|
575
|
+
workspaceId,
|
|
576
|
+
);
|
|
577
|
+
}),
|
|
578
|
+
);
|
|
579
|
+
result.filesImported += pendingWorkspaceFiles.length;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Save topic context file blobs in parallel, then update topics.json once.
|
|
443
583
|
if (contextFiles.length > 0) {
|
|
444
|
-
// Write binary files in parallel
|
|
445
584
|
const cfRecords = await Promise.all(
|
|
446
585
|
contextFiles.map(async ({ topicId, filename, buffer }) => {
|
|
447
586
|
const fileId = randomUUID();
|
|
448
|
-
await
|
|
587
|
+
const text = await extractText(buffer, filename);
|
|
588
|
+
await storage.writeOriginalBlob(fileId, buffer, workspaceId);
|
|
589
|
+
await storage.writeContextFileBlob(
|
|
590
|
+
workspaceId,
|
|
591
|
+
fileId,
|
|
592
|
+
Buffer.from(text, "utf-8"),
|
|
593
|
+
);
|
|
449
594
|
return { topicId, fileId, filename };
|
|
450
595
|
}),
|
|
451
596
|
);
|
|
@@ -697,6 +842,31 @@ async function uploadFileToFolder(
|
|
|
697
842
|
const buffer = Buffer.isBuffer(content)
|
|
698
843
|
? content
|
|
699
844
|
: Buffer.from(content, "utf-8");
|
|
845
|
+
const safeName = name.replace(/'/g, "\\'");
|
|
846
|
+
const existing = await drive.files.list({
|
|
847
|
+
q: `'${folderId}' in parents and name = '${safeName}' and trashed = false`,
|
|
848
|
+
fields: "files(id)",
|
|
849
|
+
pageSize: 10,
|
|
850
|
+
});
|
|
851
|
+
const [first, ...duplicates] = existing.data.files || [];
|
|
852
|
+
if (first?.id) {
|
|
853
|
+
await drive.files.update({
|
|
854
|
+
fileId: first.id,
|
|
855
|
+
requestBody: { name },
|
|
856
|
+
media: { mimeType, body: Readable.from(buffer) },
|
|
857
|
+
fields: "id",
|
|
858
|
+
});
|
|
859
|
+
await Promise.all(
|
|
860
|
+
duplicates.map((file) =>
|
|
861
|
+
file.id
|
|
862
|
+
? drive.files.delete({ fileId: file.id }).catch(() => {})
|
|
863
|
+
: null,
|
|
864
|
+
),
|
|
865
|
+
);
|
|
866
|
+
await ensurePublicRead(drive, first.id);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
700
870
|
const created = await drive.files.create({
|
|
701
871
|
requestBody: { name, parents: [folderId] },
|
|
702
872
|
media: { mimeType, body: Readable.from(buffer) },
|
|
@@ -731,6 +901,39 @@ export async function exportWorkspace(
|
|
|
731
901
|
const exportFolderId =
|
|
732
902
|
targetFolderId ?? (await getOrCreateFolder(drive, folderId, "_export"));
|
|
733
903
|
const topics = await storage.getTopicsForWorkspace(workspaceId);
|
|
904
|
+
const ctxDir = storage.getContextFilesDirForWorkspace(workspaceId);
|
|
905
|
+
|
|
906
|
+
const workspaceFiles = await storage.getWorkspaceContextFiles(workspaceId);
|
|
907
|
+
if (workspaceFiles.length > 0) {
|
|
908
|
+
const workspaceFilesFolderId = await getOrCreateFolder(
|
|
909
|
+
drive,
|
|
910
|
+
exportFolderId,
|
|
911
|
+
WORKSPACE_FILES_FOLDER_NAME,
|
|
912
|
+
);
|
|
913
|
+
await Promise.all(
|
|
914
|
+
workspaceFiles.map(async (cf) => {
|
|
915
|
+
try {
|
|
916
|
+
const buffer = await readContextFileExportBytes(
|
|
917
|
+
cf,
|
|
918
|
+
workspaceId,
|
|
919
|
+
ctxDir,
|
|
920
|
+
);
|
|
921
|
+
await uploadFileToFolder(
|
|
922
|
+
drive,
|
|
923
|
+
workspaceFilesFolderId,
|
|
924
|
+
cf.originalName,
|
|
925
|
+
buffer,
|
|
926
|
+
mimeForFilename(cf.originalName),
|
|
927
|
+
);
|
|
928
|
+
result.filesExported++;
|
|
929
|
+
} catch (err: any) {
|
|
930
|
+
result.errors.push(
|
|
931
|
+
`Workspace file "${cf.originalName}": ${err?.message || "upload failed"}`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
}),
|
|
935
|
+
);
|
|
936
|
+
}
|
|
734
937
|
|
|
735
938
|
for (const topic of topics) {
|
|
736
939
|
try {
|
|
@@ -761,34 +964,16 @@ export async function exportWorkspace(
|
|
|
761
964
|
? questions.find((p) => p.id === q.parentQuestionId)?.title
|
|
762
965
|
: undefined;
|
|
763
966
|
|
|
764
|
-
// Read
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
cf.origin === "sandbox" ||
|
|
775
|
-
cf.origin === "browser-security" ||
|
|
776
|
-
cf.origin === "react" ||
|
|
777
|
-
cf.origin === "nextjs" ||
|
|
778
|
-
cf.origin === "module-federation" ||
|
|
779
|
-
cf.origin === "infra"
|
|
780
|
-
) {
|
|
781
|
-
try {
|
|
782
|
-
const content = await fs.readFile(
|
|
783
|
-
path.join(ctxDir, cf.id),
|
|
784
|
-
"utf-8",
|
|
785
|
-
);
|
|
786
|
-
contextFilesWithContent.push({ ...cf, content });
|
|
787
|
-
} catch {
|
|
788
|
-
contextFilesWithContent.push(cf);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
967
|
+
// Read every question-attached context file so uploaded docs,
|
|
968
|
+
// images, and generated code snippets survive Drive sync.
|
|
969
|
+
const contextFilesWithContent = await Promise.all(
|
|
970
|
+
(q.contextFiles || []).map((cf) =>
|
|
971
|
+
serializeContextFileForExport(cf, workspaceId, ctxDir),
|
|
972
|
+
),
|
|
973
|
+
);
|
|
974
|
+
const legacyCodeSnippets = contextFilesWithContent.filter(
|
|
975
|
+
(cf) => cf.origin && cf.origin !== "upload",
|
|
976
|
+
);
|
|
792
977
|
|
|
793
978
|
// Export full question as JSON so nothing is lost on sync-back
|
|
794
979
|
const payload = JSON.stringify(
|
|
@@ -799,7 +984,8 @@ export async function exportWorkspace(
|
|
|
799
984
|
messages: q.messages,
|
|
800
985
|
codeContextFiles: q.codeContextFiles,
|
|
801
986
|
codeAnnotations: q.codeAnnotations ?? {},
|
|
802
|
-
|
|
987
|
+
contextFiles: contextFilesWithContent,
|
|
988
|
+
codeSnippets: legacyCodeSnippets,
|
|
803
989
|
createdAt: q.createdAt,
|
|
804
990
|
},
|
|
805
991
|
null,
|
|
@@ -829,17 +1015,20 @@ export async function exportWorkspace(
|
|
|
829
1015
|
topicFolderId,
|
|
830
1016
|
"context-files",
|
|
831
1017
|
);
|
|
832
|
-
const ctxDir = storage.getContextFilesDirForWorkspace(workspaceId);
|
|
833
1018
|
await Promise.all(
|
|
834
1019
|
topic.contextFiles.map(async (cf) => {
|
|
835
1020
|
try {
|
|
836
|
-
const buffer = await
|
|
1021
|
+
const buffer = await readContextFileExportBytes(
|
|
1022
|
+
cf,
|
|
1023
|
+
workspaceId,
|
|
1024
|
+
ctxDir,
|
|
1025
|
+
);
|
|
837
1026
|
await uploadFileToFolder(
|
|
838
1027
|
drive,
|
|
839
1028
|
ctxFolderId,
|
|
840
1029
|
cf.originalName,
|
|
841
1030
|
buffer,
|
|
842
|
-
|
|
1031
|
+
mimeForFilename(cf.originalName),
|
|
843
1032
|
);
|
|
844
1033
|
result.filesExported++;
|
|
845
1034
|
} catch (err: any) {
|
|
@@ -434,6 +434,7 @@ export async function clearWorkspaceData(workspaceId: string): Promise<void> {
|
|
|
434
434
|
await ensureWorkspaceDirs(workspaceId);
|
|
435
435
|
const wid = workspaceId;
|
|
436
436
|
await fs.writeFile(topicsFilePath(wid), JSON.stringify([], null, 2));
|
|
437
|
+
await fs.writeFile(workspaceFilesFilePath(wid), JSON.stringify([], null, 2));
|
|
437
438
|
await Promise.all([
|
|
438
439
|
fs
|
|
439
440
|
.readdir(questionsDirPath(wid))
|