/** * Bulk export — export crawled screens to CSV / JSON / Markdown digest. * * Outputs: * - CSV: url, name, wordCount, screenshotPath, markdownPath, createdAt — for spreadsheet analysis * - JSON: full screen records array — for API integration * - Markdown digest: concatenated markdown of all screens — for LLM context windows */ import * as fs from 'fs/promises'; import * as path from 'path'; export interface ExportScreen { id: string; name: string; url: string; projectId: string; wordCount?: number; screenshotPath?: string | null; markdownPath?: string | null; markdown?: string | null; createdAt?: Date; updatedAt?: Date; } function escapeCsv(val: string): string { if (val.includes(',') || val.includes('"') || val.includes('\n')) { return `"${val.replace(/"/g, '""')}"`; } return val; } export async function exportToCsv(screens: ExportScreen[], outputPath: string): Promise { const headers = ['id', 'name', 'url', 'projectId', 'wordCount', 'screenshotPath', 'markdownPath', 'createdAt']; const rows = screens.map(s => [ s.id, s.name, s.url, s.projectId, String(s.wordCount ?? 0), s.screenshotPath ?? '', s.markdownPath ?? '', s.createdAt?.toISOString() ?? '', ].map(escapeCsv).join(',')); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, [headers.join(','), ...rows].join('\n'), 'utf8'); } export async function exportToJson(screens: ExportScreen[], outputPath: string): Promise { await fs.mkdir(path.dirname(outputPath), { recursive: true }); const data = screens.map(s => ({ id: s.id, name: s.name, url: s.url, projectId: s.projectId, wordCount: s.wordCount, screenshotPath: s.screenshotPath, markdownPath: s.markdownPath, createdAt: s.createdAt?.toISOString(), updatedAt: s.updatedAt?.toISOString(), })); await fs.writeFile(outputPath, JSON.stringify(data, null, 2), 'utf8'); } export async function exportToMarkdownDigest( screens: ExportScreen[], outputPath: string, opts: { readMarkdownFromPath?: boolean; maxScreens?: number } = {}, ): Promise { const { readMarkdownFromPath = true, maxScreens = 200 } = opts; await fs.mkdir(path.dirname(outputPath), { recursive: true }); const handle = await fs.open(outputPath, 'w'); try { await handle.write(`# ZeTa Crawl Digest\n\nExported: ${new Date().toISOString()}\nScreens: ${Math.min(screens.length, maxScreens)}\n\n---\n\n`); for (const screen of screens.slice(0, maxScreens)) { let md = screen.markdown ?? ''; if (!md && readMarkdownFromPath && screen.markdownPath) { md = await fs.readFile(screen.markdownPath, 'utf8').catch(() => ''); } await handle.write(`## ${screen.name}\n\n**URL:** ${screen.url}\n\n${md.trim()}\n\n---\n\n`); } } finally { await handle.close(); } }