profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
export.ts 3,159 bytes
1 import { readFile, stat } from 'node:fs/promises'
2 import path from 'node:path'
3 import type { FastifyInstance } from 'fastify'
4 import { formatTranscript } from '../../shared/transcript'
5 import { createZip, type ZipEntryInput } from '../exporter/zip'
6 import { SPEC_PACK_FILES } from '../providers/types'
7 import type { SessionStore } from '../store/sessionStore'
8
9 const SOURCES_FILE = 'sources.json'
10 const SPEC_PACK_EXPORT_PATH = '/api/sessions/:id/export/spec-pack.zip'
11 const SPEC_PACK_EXPORT_FILES = [...SPEC_PACK_FILES, SOURCES_FILE]
12
13 export interface ExportRouteDeps {
14 store: SessionStore
15 }
16
17 function slugify(name: string): string {
18 const slug = name
19 .toLowerCase()
20 .replace(/[^a-z0-9]+/g, '-')
21 .replace(/^-+|-+$/g, '')
22 return slug.length > 0 ? slug : 'voicetask'
23 }
24
25 async function hasCompleteSpecPack(targetDir: string): Promise<boolean> {
26 const specDir = path.join(targetDir, 'spec')
27 const checks = await Promise.all(
28 SPEC_PACK_EXPORT_FILES.map((file) =>
29 stat(path.join(specDir, file))
30 .then((fileStat) => fileStat.isFile())
31 .catch(() => false),
32 ),
33 )
34 return checks.every(Boolean)
35 }
36
37 export function registerExportRoutes(app: FastifyInstance, deps: ExportRouteDeps): void {
38 const { store } = deps
39
40 app.head<{ Params: { id: string } }>(SPEC_PACK_EXPORT_PATH, async (request, reply) => {
41 const session = await store.getSession(request.params.id)
42 if (!session) return reply.code(404).send({ error: 'session not found' })
43
44 if (!(await hasCompleteSpecPack(session.targetDir))) {
45 return reply.code(404).send({ error: 'no complete spec pack found yet, generate it first' })
46 }
47 return reply.code(200).send()
48 })
49
50 app.get<{ Params: { id: string } }>(
51 SPEC_PACK_EXPORT_PATH,
52 { exposeHeadRoute: false },
53 async (request, reply) => {
54 const session = await store.getSession(request.params.id)
55 if (!session) return reply.code(404).send({ error: 'session not found' })
56
57 if (!(await hasCompleteSpecPack(session.targetDir))) {
58 return reply.code(404).send({ error: 'no complete spec pack found yet, generate it first' })
59 }
60
61 const specDir = path.join(session.targetDir, 'spec')
62 const entries: ZipEntryInput[] = []
63 for (const file of SPEC_PACK_EXPORT_FILES) {
64 entries.push({ name: file, content: await readFile(path.join(specDir, file)) })
65 }
66
67 const zipBuffer = createZip(entries)
68 reply.header('content-type', 'application/zip')
69 reply.header('content-disposition', `attachment; filename="${slugify(session.name)}-spec-pack.zip"`)
70 return reply.send(zipBuffer)
71 },
72 )
73
74 app.get<{ Params: { id: string } }>('/api/sessions/:id/export/transcript.md', async (request, reply) => {
75 const session = await store.getSession(request.params.id)
76 if (!session) return reply.code(404).send({ error: 'session not found' })
77
78 const text = formatTranscript(session.segments, session.name)
79 reply.header('content-type', 'text/markdown; charset=utf-8')
80 reply.header('content-disposition', `attachment; filename="${slugify(session.name)}-transcript.md"`)
81 return reply.send(text)
82 })
83 }
84