@bendyline/squisq-cli 1.0.0 → 1.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 (41) hide show
  1. package/README.md +121 -0
  2. package/dist/__tests__/convert.test.d.ts +7 -0
  3. package/dist/__tests__/convert.test.d.ts.map +1 -0
  4. package/dist/__tests__/convert.test.js +127 -0
  5. package/dist/__tests__/convert.test.js.map +1 -0
  6. package/dist/__tests__/readInput.test.d.ts +5 -0
  7. package/dist/__tests__/readInput.test.d.ts.map +1 -0
  8. package/dist/__tests__/readInput.test.js +120 -0
  9. package/dist/__tests__/readInput.test.js.map +1 -0
  10. package/dist/api.d.ts +106 -0
  11. package/dist/api.d.ts.map +1 -0
  12. package/dist/api.js +324 -0
  13. package/dist/api.js.map +1 -0
  14. package/dist/commands/convert.d.ts +15 -0
  15. package/dist/commands/convert.d.ts.map +1 -0
  16. package/dist/commands/convert.js +186 -0
  17. package/dist/commands/convert.js.map +1 -0
  18. package/dist/commands/video.d.ts +12 -0
  19. package/dist/commands/video.d.ts.map +1 -0
  20. package/dist/commands/video.js +122 -0
  21. package/dist/commands/video.js.map +1 -0
  22. package/dist/index.d.ts +12 -2
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +20 -425
  25. package/dist/index.js.map +1 -1
  26. package/dist/nativeEncoder-EXDP2O5B.js +0 -0
  27. package/dist/nativeEncoder-MWHOONST.js +88 -0
  28. package/dist/nativeEncoder-MWHOONST.js.map +1 -0
  29. package/dist/util/detectFfmpeg.d.ts +15 -0
  30. package/dist/util/detectFfmpeg.d.ts.map +1 -0
  31. package/dist/util/detectFfmpeg.js +29 -0
  32. package/dist/util/detectFfmpeg.js.map +1 -0
  33. package/dist/util/nativeEncoder.d.ts +27 -0
  34. package/dist/util/nativeEncoder.d.ts.map +1 -0
  35. package/dist/util/nativeEncoder.js +106 -0
  36. package/dist/util/nativeEncoder.js.map +1 -0
  37. package/dist/util/readInput.d.ts +31 -0
  38. package/dist/util/readInput.d.ts.map +1 -0
  39. package/dist/util/readInput.js +142 -0
  40. package/dist/util/readInput.js.map +1 -0
  41. package/package.json +17 -5
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/convert.ts","../src/util/readInput.ts","../src/commands/video.ts","../src/util/detectFfmpeg.ts"],"sourcesContent":["/**\n * squisq CLI\n *\n * Command-line tool for converting and processing Squisq documents.\n * Designed for easy addition of future subcommands (e.g., import).\n *\n * Usage:\n * squisq convert <input> [options]\n * squisq --help\n */\n\nimport { Command } from 'commander';\nimport { registerConvertCommand } from './commands/convert.js';\nimport { registerVideoCommand } from './commands/video.js';\n\n// Colored banner: cyan brackets, bold white text, dim version\nconsole.error(\n '\\x1b[36m{[\\x1b[0m \\x1b[1msquiggly square\\x1b[0m \\x1b[2m—\\x1b[0m \\x1b[1msquisq\\x1b[0m \\x1b[2m—\\x1b[0m \\x1b[33mv1.0.0\\x1b[0m \\x1b[36m]}\\x1b[0m',\n);\n\nconst program = new Command();\n\nprogram\n .name('squisq')\n .description('Squisq CLI — convert and process markdown-based documents')\n .version('1.0.0');\n\nregisterConvertCommand(program);\nregisterVideoCommand(program);\n\nprogram.parse();\n","/**\n * convert command\n *\n * Reads a markdown file, ZIP/DBK container, or folder and exports to\n * all supported formats: DOCX, PDF, HTML, and container ZIP (.dbk).\n *\n * Usage:\n * squisq convert <input> [--output-dir <dir>] [--formats <list>]\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { dirname, basename, extname, join, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport { readInput } from '../util/readInput.js';\n\nconst ALL_FORMATS = ['docx', 'pdf', 'html', 'dbk'] as const;\ntype Format = (typeof ALL_FORMATS)[number];\n\nfunction parseFormats(value: string): Format[] {\n const requested = value.split(',').map((s) => s.trim().toLowerCase());\n const valid: Format[] = [];\n for (const r of requested) {\n if (ALL_FORMATS.includes(r as Format)) {\n valid.push(r as Format);\n } else {\n console.warn(`Unknown format \"${r}\" — skipping. Valid: ${ALL_FORMATS.join(', ')}`);\n }\n }\n if (valid.length === 0) {\n throw new Error(`No valid formats specified. Valid: ${ALL_FORMATS.join(', ')}`);\n }\n return valid;\n}\n\nexport function registerConvertCommand(program: Command): void {\n program\n .command('convert')\n .description('Convert a markdown document to DOCX, PDF, HTML, and DBK container formats')\n .argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')\n .option('-o, --output-dir <dir>', 'Output directory (default: same as input)')\n .option(\n '-f, --formats <list>',\n `Comma-separated formats to produce (default: all). Valid: ${ALL_FORMATS.join(', ')}`,\n )\n .action(async (inputPath: string, opts: { outputDir?: string; formats?: string }) => {\n try {\n await runConvert(inputPath, opts);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n process.exitCode = 1;\n }\n });\n}\n\nasync function runConvert(\n inputPath: string,\n opts: { outputDir?: string; formats?: string },\n): Promise<void> {\n const resolvedInput = resolve(inputPath);\n\n // Determine which formats to produce\n const formats: Format[] = opts.formats ? parseFormats(opts.formats) : [...ALL_FORMATS];\n\n // Determine output directory and base filename\n const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput);\n const inputBasename = basename(resolvedInput);\n const inputExt = extname(inputBasename);\n const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;\n\n // Ensure output directory exists\n await mkdir(outputDir, { recursive: true });\n\n console.error(`Reading: ${resolvedInput}`);\n const { container, markdownDoc } = await readInput(resolvedInput);\n\n for (const format of formats) {\n const outPath = join(outputDir, `${baseName}.${format}`);\n\n switch (format) {\n case 'docx': {\n const { markdownDocToDocx } = await import('@bendyline/squisq-formats/docx');\n const buf = await markdownDocToDocx(markdownDoc);\n await writeFile(outPath, Buffer.from(buf));\n break;\n }\n\n case 'pdf': {\n const { markdownDocToPdf } = await import('@bendyline/squisq-formats/pdf');\n const buf = await markdownDocToPdf(markdownDoc);\n await writeFile(outPath, Buffer.from(buf));\n break;\n }\n\n case 'html': {\n const { markdownToDoc } = await import('@bendyline/squisq/doc');\n const { docToHtml, collectImagePaths } = await import('@bendyline/squisq-formats/html');\n const { PLAYER_BUNDLE } = await import('@bendyline/squisq-react/standalone-source');\n\n const doc = markdownToDoc(markdownDoc);\n\n // Gather images referenced by the doc from the container\n const imagePaths = collectImagePaths(doc);\n const images = new Map<string, ArrayBuffer>();\n for (const imgPath of imagePaths) {\n const data = await container.readFile(imgPath);\n if (data) {\n images.set(imgPath, data);\n }\n }\n\n const html = docToHtml(doc, {\n playerScript: PLAYER_BUNDLE,\n images,\n title: baseName,\n mode: 'static',\n });\n await writeFile(outPath, html, 'utf-8');\n break;\n }\n\n case 'dbk': {\n const { containerToZip } = await import('@bendyline/squisq-formats/container');\n const blob = await containerToZip(container);\n const buf = await blob.arrayBuffer();\n await writeFile(outPath, Buffer.from(buf));\n break;\n }\n }\n\n console.error(` ✓ ${outPath}`);\n }\n\n console.error('Done.');\n}\n","/**\n * readInput\n *\n * Unified input reader for the CLI. Accepts a path to a .md file,\n * .zip/.dbk container, or a folder and returns a MemoryContentContainer\n * (virtual file system) plus the parsed MarkdownDocument.\n */\n\nimport { readFile, readdir, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { parseMarkdown } from '@bendyline/squisq/markdown';\nimport type { MarkdownDocument } from '@bendyline/squisq/markdown';\nimport { MemoryContentContainer } from '@bendyline/squisq/storage';\nimport { zipToContainer } from '@bendyline/squisq-formats/container';\n\nexport interface ReadInputResult {\n container: MemoryContentContainer;\n markdownDoc: MarkdownDocument;\n}\n\n/** MIME type lookup by extension (common content types) */\nconst MIME_TYPES: Record<string, string> = {\n '.md': 'text/markdown',\n '.txt': 'text/plain',\n '.json': 'application/json',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.svg': 'image/svg+xml',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.ogg': 'audio/ogg',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n};\n\nfunction mimeFromExt(filePath: string): string {\n return MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/**\n * Recursively walk a directory and return all file paths (relative to root).\n */\nasync function walkDir(root: string, prefix = ''): Promise<string[]> {\n const entries = await readdir(root, { withFileTypes: true });\n const paths: string[] = [];\n\n for (const entry of entries) {\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n paths.push(...(await walkDir(join(root, entry.name), relPath)));\n } else if (entry.isFile()) {\n paths.push(relPath);\n }\n }\n\n return paths;\n}\n\n/**\n * Read input from a file path (markdown, ZIP/DBK container, or folder)\n * and return a populated ContentContainer + parsed MarkdownDocument.\n */\nexport async function readInput(inputPath: string): Promise<ReadInputResult> {\n const info = await stat(inputPath);\n\n if (info.isDirectory()) {\n return readFolder(inputPath);\n }\n\n const ext = extname(inputPath).toLowerCase();\n if (ext === '.zip' || ext === '.dbk') {\n return readContainer(inputPath);\n }\n\n // Default: treat as a markdown file\n return readMarkdownFile(inputPath);\n}\n\nasync function readMarkdownFile(filePath: string): Promise<ReadInputResult> {\n const content = await readFile(filePath, 'utf-8');\n const container = new MemoryContentContainer();\n await container.writeDocument(content);\n const markdownDoc = parseMarkdown(content);\n return { container, markdownDoc };\n}\n\nasync function readContainer(filePath: string): Promise<ReadInputResult> {\n const data = await readFile(filePath);\n const container = await zipToContainer(\n data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer,\n );\n\n const markdown = await container.readDocument();\n if (!markdown) {\n throw new Error(`No markdown document found in container: ${filePath}`);\n }\n\n const markdownDoc = parseMarkdown(markdown);\n return { container, markdownDoc };\n}\n\nasync function readFolder(dirPath: string): Promise<ReadInputResult> {\n const container = new MemoryContentContainer();\n const files = await walkDir(dirPath);\n\n for (const relPath of files) {\n const absPath = join(dirPath, relPath);\n const data = await readFile(absPath);\n await container.writeFile(\n relPath,\n new Uint8Array(data.buffer, data.byteOffset, data.byteLength),\n mimeFromExt(relPath),\n );\n }\n\n const markdown = await container.readDocument();\n if (!markdown) {\n throw new Error(`No markdown document found in folder: ${dirPath}`);\n }\n\n const markdownDoc = parseMarkdown(markdown);\n return { container, markdownDoc };\n}\n","/**\n * video command\n *\n * Renders a squisq document to MP4 video by:\n * 1. Parsing the input (markdown, container, or folder)\n * 2. Generating a self-contained render HTML\n * 3. Capturing frames via Playwright headless browser\n * 4. Encoding to MP4 using native ffmpeg (fast) or ffmpeg.wasm (fallback)\n *\n * Usage:\n * squisq video <input> [-o output.mp4] [--fps 30] [--quality normal] [--orientation landscape]\n */\n\nimport { mkdir } from 'node:fs/promises';\nimport { dirname, basename, extname, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport { readInput } from '../util/readInput.js';\nimport { detectFfmpeg } from '../util/detectFfmpeg.js';\n\nimport type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\n\ntype CaptionOption = 'off' | 'standard' | 'social';\n\ninterface VideoCommandOptions {\n output?: string;\n fps?: string;\n quality?: VideoQuality;\n orientation?: VideoOrientation;\n captions?: CaptionOption;\n width?: string;\n height?: string;\n}\n\nconst VALID_QUALITIES = ['draft', 'normal', 'high'] as const;\nconst VALID_ORIENTATIONS = ['landscape', 'portrait'] as const;\nconst VALID_CAPTIONS = ['off', 'standard', 'social'] as const;\n\nexport function registerVideoCommand(program: Command): void {\n program\n .command('video')\n .description('Render a squisq document to MP4 video')\n .argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')\n .argument('[output]', 'Output MP4 path (default: <input>.mp4)')\n .option('-o, --output <path>', 'Output MP4 path (default: <input>.mp4)')\n .option('--fps <number>', 'Frames per second (default: 30)', '30')\n .option(\n '--quality <level>',\n `Encoding quality: ${VALID_QUALITIES.join(', ')} (default: normal)`,\n 'normal',\n )\n .option(\n '--orientation <orient>',\n `Video orientation: ${VALID_ORIENTATIONS.join(', ')} (default: landscape)`,\n 'landscape',\n )\n .option(\n '--captions <style>',\n `Caption style: ${VALID_CAPTIONS.join(', ')} (default: off)`,\n 'off',\n )\n .option('--width <pixels>', 'Override video width')\n .option('--height <pixels>', 'Override video height')\n .action(async (inputPath: string, outputArg: string | undefined, opts: VideoCommandOptions) => {\n try {\n // Positional output arg takes precedence, then -o flag\n if (outputArg && !opts.output) {\n opts.output = outputArg;\n }\n await runVideo(inputPath, opts);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n process.exitCode = 1;\n }\n });\n}\n\nasync function runVideo(inputPath: string, opts: VideoCommandOptions): Promise<void> {\n const resolvedInput = resolve(inputPath);\n\n // Validate options\n const fps = parseInt(opts.fps ?? '30', 10);\n if (isNaN(fps) || fps < 1 || fps > 120) {\n throw new Error('FPS must be a number between 1 and 120');\n }\n\n const quality = opts.quality ?? 'normal';\n if (!VALID_QUALITIES.includes(quality as (typeof VALID_QUALITIES)[number])) {\n throw new Error(`Invalid quality \"${quality}\". Valid: ${VALID_QUALITIES.join(', ')}`);\n }\n\n const orientation = opts.orientation ?? 'landscape';\n if (!VALID_ORIENTATIONS.includes(orientation as (typeof VALID_ORIENTATIONS)[number])) {\n throw new Error(\n `Invalid orientation \"${orientation}\". Valid: ${VALID_ORIENTATIONS.join(', ')}`,\n );\n }\n\n const captions = opts.captions ?? 'off';\n if (!VALID_CAPTIONS.includes(captions as (typeof VALID_CAPTIONS)[number])) {\n throw new Error(`Invalid captions \"${captions}\". Valid: ${VALID_CAPTIONS.join(', ')}`);\n }\n const captionStyle = captions === 'off' ? undefined : (captions as 'standard' | 'social');\n\n // Determine output path\n const inputBasename = basename(resolvedInput);\n const inputExt = extname(inputBasename);\n const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;\n const outputPath = opts.output\n ? resolve(opts.output)\n : resolve(dirname(resolvedInput), `${baseName}.mp4`);\n\n // Ensure output directory exists\n await mkdir(dirname(outputPath), { recursive: true });\n\n // ── Step 1: Read input ──────────────────────────────────────────\n console.error(`Reading: ${resolvedInput}`);\n const { container, markdownDoc } = await readInput(resolvedInput);\n\n // ── Step 2: Parse to Doc ────────────────────────────────────────\n const { markdownToDoc } = await import('@bendyline/squisq/doc');\n const doc: Doc = markdownToDoc(markdownDoc);\n\n // ── Step 3: Collect media from container ────────────────────────\n const { collectImagePaths } = await import('@bendyline/squisq-formats/html');\n const imagePaths = collectImagePaths(doc);\n const images = new Map<string, ArrayBuffer>();\n for (const imgPath of imagePaths) {\n const data = await container.readFile(imgPath);\n if (data) {\n images.set(imgPath, data);\n }\n }\n\n // Collect audio segments\n const audio = new Map<string, ArrayBuffer>();\n let concatenatedAudio: Uint8Array | null = null;\n if (doc.audio?.segments?.length) {\n for (const seg of doc.audio.segments) {\n const data = await container.readFile(seg.src);\n if (data) {\n audio.set(seg.src, data);\n // Also map by name for the audio URL rewriting in the player\n audio.set(seg.name, data);\n }\n }\n // For encoding, concatenate audio segments into one file\n // The player handles segment playback internally, but the encoder\n // needs a single audio track. We'll use the first segment as a\n // representative — full audio concatenation would need ffmpeg.\n // For now, pass audio map to the render HTML so the player handles it.\n if (audio.size > 0) {\n // Use the first segment's raw bytes — the encoder will handle it\n const firstSeg = doc.audio.segments[0];\n const firstData = await container.readFile(firstSeg.src);\n if (firstData) {\n concatenatedAudio = new Uint8Array(firstData);\n }\n }\n }\n\n // ── Step 4: Generate render HTML ────────────────────────────────\n const { generateRenderHtml } = await import('@bendyline/squisq-video');\n const { PLAYER_BUNDLE } = await import('@bendyline/squisq-react/standalone-source');\n const { resolveDimensions } = await import('@bendyline/squisq-video');\n\n const dimensions = resolveDimensions({\n orientation,\n width: opts.width ? parseInt(opts.width, 10) : undefined,\n height: opts.height ? parseInt(opts.height, 10) : undefined,\n });\n\n const renderHtml = generateRenderHtml(doc, {\n playerScript: PLAYER_BUNDLE,\n images,\n audio: audio.size > 0 ? audio : undefined,\n width: dimensions.width,\n height: dimensions.height,\n captionStyle,\n });\n\n console.error(\n `Viewport: ${dimensions.width}x${dimensions.height}, ${fps} fps, quality: ${quality}, captions: ${captions}`,\n );\n\n // ── Step 5: Capture frames via Playwright ───────────────────────\n const { chromium } = await import('playwright-core');\n const browser = await chromium.launch({ headless: true });\n const page = await browser.newPage({\n viewport: { width: dimensions.width, height: dimensions.height },\n });\n\n // Capture page errors so they don't vanish silently\n const pageErrors: string[] = [];\n page.on('pageerror', (err) => pageErrors.push(err.message));\n page.on('console', (msg) => {\n if (msg.type() === 'error') pageErrors.push(msg.text());\n });\n\n // Load the render HTML — use 'load' since everything is inline (no network)\n await page.setContent(renderHtml, { waitUntil: 'load' });\n\n // Give React one frame to mount and run useEffect\n await page.waitForTimeout(500);\n\n // Wait for the render API to be available\n try {\n await page.waitForFunction(\n () => typeof (window as unknown as Record<string, unknown>).getDuration === 'function',\n { timeout: 15000 },\n );\n } catch {\n await browser.close();\n const errorDetail = pageErrors.length\n ? `\\nPage errors:\\n ${pageErrors.join('\\n ')}`\n : '\\nNo page errors captured — the player may have failed to mount.';\n throw new Error(`Render API did not initialize within 15 seconds.${errorDetail}`);\n }\n\n // Get duration from the player\n const duration: number = await page.evaluate(() => {\n return (window as unknown as { getDuration: () => number }).getDuration();\n });\n\n if (duration <= 0) {\n await browser.close();\n throw new Error('Document has zero duration — nothing to render');\n }\n\n const totalFrames = Math.ceil(duration * fps);\n console.error(`Duration: ${duration.toFixed(1)}s, ${totalFrames} frames to capture`);\n\n const frames: Uint8Array[] = [];\n const frameInterval = 1 / fps;\n\n // Optional: render cover frame (time 0)\n const hasCover: boolean = await page.evaluate(() => {\n const w = window as unknown as { hasCoverBlock?: () => boolean };\n return typeof w.hasCoverBlock === 'function' ? w.hasCoverBlock() : false;\n });\n\n if (hasCover) {\n const coverFrameCount = 2 * fps;\n await page.evaluate(() => {\n (window as unknown as { showCover: () => void }).showCover();\n });\n await page.waitForTimeout(100);\n const coverScreenshot = await page.screenshot({ type: 'png' });\n const coverFrame = new Uint8Array(coverScreenshot);\n for (let i = 0; i < coverFrameCount; i++) {\n frames.push(coverFrame);\n }\n await page.evaluate(() => {\n (window as unknown as { hideCover: () => void }).hideCover();\n });\n writeProgress('Capturing', coverFrameCount, totalFrames + coverFrameCount);\n }\n\n // Capture animation frames\n const totalWithCover = totalFrames + frames.length;\n for (let i = 0; i < totalFrames; i++) {\n const time = i * frameInterval;\n\n await page.evaluate((t: number) => {\n return (window as unknown as { seekTo: (t: number) => Promise<void> }).seekTo(t);\n }, time);\n\n const screenshot = await page.screenshot({ type: 'png' });\n frames.push(new Uint8Array(screenshot));\n\n if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === totalFrames - 1) {\n writeProgress('Capturing', frames.length, totalWithCover);\n }\n }\n\n clearProgress();\n await browser.close();\n\n // ── Step 6: Encode to MP4 ───────────────────────────────────────\n const ffmpegPath = await detectFfmpeg();\n\n const exportOptions = {\n fps,\n quality: quality as VideoQuality,\n orientation: orientation as VideoOrientation,\n width: dimensions.width,\n height: dimensions.height,\n onProgress: (percent: number, phase: string) => {\n writeProgress('Encoding', percent, 100, phase);\n },\n };\n\n if (ffmpegPath) {\n console.error(`Using native ffmpeg: ${ffmpegPath}`);\n const { framesToMp4Native } = await import('../util/nativeEncoder.js');\n await framesToMp4Native(ffmpegPath, frames, concatenatedAudio, outputPath, exportOptions);\n } else {\n throw new Error(\n 'ffmpeg is required but not found in PATH.\\n' +\n 'Install it with:\\n' +\n ' macOS: brew install ffmpeg\\n' +\n ' Ubuntu: sudo apt install ffmpeg\\n' +\n ' Windows: winget install ffmpeg',\n );\n }\n\n clearProgress();\n console.error(` ✓ ${outputPath}`);\n console.error('Done.');\n}\n\n// ── Progress helpers ──────────────────────────────────────────────\n\nconst BAR_WIDTH = 30;\n\nfunction writeProgress(label: string, current: number, total: number, detail?: string): void {\n const pct = Math.min(100, Math.round((current / total) * 100));\n const filled = Math.round((pct / 100) * BAR_WIDTH);\n const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);\n const suffix = detail ? ` (${detail})` : '';\n process.stderr.write(`\\r ${label}: ${bar} ${pct}%${suffix} `);\n}\n\nfunction clearProgress(): void {\n process.stderr.write('\\r' + ' '.repeat(80) + '\\r');\n}\n","/**\n * detectFfmpeg\n *\n * Checks whether a native ffmpeg binary is available on the system PATH.\n * Returns the absolute path to the binary if found, or null if not.\n *\n * Uses `which` on macOS/Linux and `where` on Windows.\n */\n\nimport { execFile } from 'node:child_process';\n\n/**\n * Detect whether native ffmpeg is installed and available on PATH.\n *\n * @returns Absolute path to ffmpeg binary, or null if not found\n */\nexport async function detectFfmpeg(): Promise<string | null> {\n const command = process.platform === 'win32' ? 'where' : 'which';\n\n return new Promise((resolve) => {\n execFile(command, ['ffmpeg'], { timeout: 5000 }, (err, stdout) => {\n if (err || !stdout.trim()) {\n resolve(null);\n return;\n }\n // `which` and `where` can return multiple lines; take the first\n const path = stdout.trim().split('\\n')[0].trim();\n resolve(path);\n });\n });\n}\n"],"mappings":"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","names":["extname","join","program","extname","join","mkdir","dirname","basename","extname","resolve","resolve","program","resolve","basename","extname","dirname","mkdir"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE3D,8DAA8D;AAC9D,OAAO,CAAC,KAAK,CACX,8IAA8I,CAC/I,CAAC;AAEF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,2DAA2D,CAAC;KACxE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AAE9B,OAAO,CAAC,KAAK,EAAE,CAAC"}
File without changes
@@ -0,0 +1,88 @@
1
+ // src/util/nativeEncoder.ts
2
+ import { execFile } from "child_process";
3
+ import { writeFile, readFile, mkdir, rm } from "fs/promises";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+ import { randomBytes } from "crypto";
7
+ import { QUALITY_PRESETS, resolveDimensions } from "@bendyline/squisq-video";
8
+ async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
9
+ const fps = options.fps ?? 30;
10
+ const quality = options.quality ?? "normal";
11
+ const { width, height } = resolveDimensions(options);
12
+ const preset = QUALITY_PRESETS[quality];
13
+ const onProgress = options.onProgress;
14
+ if (frames.length === 0) {
15
+ throw new Error("No frames provided for encoding");
16
+ }
17
+ const tmpId = randomBytes(8).toString("hex");
18
+ const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
19
+ await mkdir(workDir, { recursive: true });
20
+ try {
21
+ onProgress?.(0, "writing frames");
22
+ const padLen = String(frames.length).length;
23
+ for (let i = 0; i < frames.length; i++) {
24
+ const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
25
+ await writeFile(join(workDir, name), frames[i]);
26
+ if (onProgress && i % 10 === 0) {
27
+ onProgress(Math.round(i / frames.length * 30), "writing frames");
28
+ }
29
+ }
30
+ let audioPath = null;
31
+ if (audio) {
32
+ audioPath = join(workDir, "audio-input");
33
+ await writeFile(audioPath, audio);
34
+ }
35
+ onProgress?.(30, "encoding");
36
+ const padPattern = join(workDir, `frame-%0${padLen}d.png`);
37
+ const args = ["-y", "-framerate", String(fps), "-i", padPattern];
38
+ if (audioPath) {
39
+ args.push("-i", audioPath);
40
+ }
41
+ args.push(
42
+ "-c:v",
43
+ "libx264",
44
+ "-preset",
45
+ preset.preset,
46
+ "-crf",
47
+ String(preset.crf),
48
+ "-pix_fmt",
49
+ "yuv420p",
50
+ "-vf",
51
+ `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
52
+ );
53
+ if (audioPath) {
54
+ args.push("-c:a", "aac", "-b:a", "128k", "-shortest");
55
+ }
56
+ args.push(outputPath);
57
+ await new Promise((resolve, reject) => {
58
+ const proc = execFile(ffmpegPath, args, { timeout: 6e5 }, (err) => {
59
+ if (err) {
60
+ reject(new Error(`ffmpeg failed: ${err.message}`));
61
+ } else {
62
+ resolve();
63
+ }
64
+ });
65
+ proc.stderr?.on("data", () => {
66
+ });
67
+ });
68
+ onProgress?.(100, "done");
69
+ } finally {
70
+ await rm(workDir, { recursive: true, force: true });
71
+ }
72
+ }
73
+ async function framesToMp4NativeBytes(ffmpegPath, frames, audio, options = {}) {
74
+ const tmpId = randomBytes(8).toString("hex");
75
+ const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);
76
+ try {
77
+ await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);
78
+ const data = await readFile(tmpOutput);
79
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
80
+ } finally {
81
+ await rm(tmpOutput, { force: true });
82
+ }
83
+ }
84
+ export {
85
+ framesToMp4Native,
86
+ framesToMp4NativeBytes
87
+ };
88
+ //# sourceMappingURL=nativeEncoder-MWHOONST.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/nativeEncoder.ts"],"sourcesContent":["/**\n * Native FFmpeg Encoder\n *\n * Encodes PNG frames to MP4 using a locally installed ffmpeg binary.\n * Writes frames to a temporary directory, invokes ffmpeg as a child process,\n * and reads the resulting MP4.\n *\n * This is the fast path — used when native ffmpeg is detected on the system.\n * Falls back to the WASM encoder (in @bendyline/squisq-video) when unavailable.\n */\n\nimport { execFile } from 'node:child_process';\nimport { writeFile, readFile, mkdir, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { randomBytes } from 'node:crypto';\n\nimport type { VideoExportOptions } from '@bendyline/squisq-video';\nimport { QUALITY_PRESETS, resolveDimensions } from '@bendyline/squisq-video';\n\n/**\n * Encode frame PNGs to MP4 using native ffmpeg.\n *\n * @param ffmpegPath - Absolute path to the ffmpeg binary\n * @param frames - Array of PNG image bytes (one per frame)\n * @param audio - Optional audio file bytes to mux\n * @param outputPath - Where to write the final MP4\n * @param options - Encoding options (fps, quality, dimensions, progress)\n */\nexport async function framesToMp4Native(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n outputPath: string,\n options: VideoExportOptions = {},\n): Promise<void> {\n const fps = options.fps ?? 30;\n const quality = options.quality ?? 'normal';\n const { width, height } = resolveDimensions(options);\n const preset = QUALITY_PRESETS[quality];\n const onProgress = options.onProgress;\n\n if (frames.length === 0) {\n throw new Error('No frames provided for encoding');\n }\n\n // Create a temp working directory\n const tmpId = randomBytes(8).toString('hex');\n const workDir = join(tmpdir(), `squisq-video-${tmpId}`);\n await mkdir(workDir, { recursive: true });\n\n try {\n onProgress?.(0, 'writing frames');\n\n // Write frame PNGs to temp directory\n const padLen = String(frames.length).length;\n for (let i = 0; i < frames.length; i++) {\n const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;\n await writeFile(join(workDir, name), frames[i]);\n\n if (onProgress && i % 10 === 0) {\n onProgress(Math.round((i / frames.length) * 30), 'writing frames');\n }\n }\n\n // Write audio if provided\n let audioPath: string | null = null;\n if (audio) {\n audioPath = join(workDir, 'audio-input');\n await writeFile(audioPath, audio);\n }\n\n onProgress?.(30, 'encoding');\n\n // Build ffmpeg arguments\n const padPattern = join(workDir, `frame-%0${padLen}d.png`);\n const args = ['-y', '-framerate', String(fps), '-i', padPattern];\n\n if (audioPath) {\n args.push('-i', audioPath);\n }\n\n args.push(\n '-c:v',\n 'libx264',\n '-preset',\n preset.preset,\n '-crf',\n String(preset.crf),\n '-pix_fmt',\n 'yuv420p',\n '-vf',\n `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`,\n );\n\n if (audioPath) {\n args.push('-c:a', 'aac', '-b:a', '128k', '-shortest');\n }\n\n args.push(outputPath);\n\n // Run ffmpeg\n await new Promise<void>((resolve, reject) => {\n const proc = execFile(ffmpegPath, args, { timeout: 600_000 }, (err) => {\n if (err) {\n reject(new Error(`ffmpeg failed: ${err.message}`));\n } else {\n resolve();\n }\n });\n\n // ffmpeg writes progress to stderr\n proc.stderr?.on('data', () => {\n // Could parse ffmpeg progress output here in the future\n });\n });\n\n onProgress?.(100, 'done');\n } finally {\n // Clean up temp directory\n await rm(workDir, { recursive: true, force: true });\n }\n}\n\n/**\n * Encode frames using native ffmpeg and return the MP4 bytes (instead of writing to disk).\n * Useful when the caller needs the bytes in memory.\n */\nexport async function framesToMp4NativeBytes(\n ffmpegPath: string,\n frames: Uint8Array[],\n audio: Uint8Array | null,\n options: VideoExportOptions = {},\n): Promise<Uint8Array> {\n const tmpId = randomBytes(8).toString('hex');\n const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);\n\n try {\n await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);\n const data = await readFile(tmpOutput);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(tmpOutput, { force: true });\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * detectFfmpeg
3
+ *
4
+ * Checks whether a native ffmpeg binary is available on the system PATH.
5
+ * Returns the absolute path to the binary if found, or null if not.
6
+ *
7
+ * Uses `which` on macOS/Linux and `where` on Windows.
8
+ */
9
+ /**
10
+ * Detect whether native ffmpeg is installed and available on PATH.
11
+ *
12
+ * @returns Absolute path to ffmpeg binary, or null if not found
13
+ */
14
+ export declare function detectFfmpeg(): Promise<string | null>;
15
+ //# sourceMappingURL=detectFfmpeg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectFfmpeg.d.ts","sourceRoot":"","sources":["../../src/util/detectFfmpeg.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;GAIG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAc3D"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * detectFfmpeg
3
+ *
4
+ * Checks whether a native ffmpeg binary is available on the system PATH.
5
+ * Returns the absolute path to the binary if found, or null if not.
6
+ *
7
+ * Uses `which` on macOS/Linux and `where` on Windows.
8
+ */
9
+ import { execFile } from 'node:child_process';
10
+ /**
11
+ * Detect whether native ffmpeg is installed and available on PATH.
12
+ *
13
+ * @returns Absolute path to ffmpeg binary, or null if not found
14
+ */
15
+ export async function detectFfmpeg() {
16
+ const command = process.platform === 'win32' ? 'where' : 'which';
17
+ return new Promise((resolve) => {
18
+ execFile(command, ['ffmpeg'], { timeout: 5000 }, (err, stdout) => {
19
+ if (err || !stdout.trim()) {
20
+ resolve(null);
21
+ return;
22
+ }
23
+ // `which` and `where` can return multiple lines; take the first
24
+ const path = stdout.trim().split('\n')[0].trim();
25
+ resolve(path);
26
+ });
27
+ });
28
+ }
29
+ //# sourceMappingURL=detectFfmpeg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectFfmpeg.js","sourceRoot":"","sources":["../../src/util/detectFfmpeg.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAEjE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC/D,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC;gBACd,OAAO;YACT,CAAC;YACD,gEAAgE;YAChE,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Native FFmpeg Encoder
3
+ *
4
+ * Encodes PNG frames to MP4 using a locally installed ffmpeg binary.
5
+ * Writes frames to a temporary directory, invokes ffmpeg as a child process,
6
+ * and reads the resulting MP4.
7
+ *
8
+ * This is the fast path — used when native ffmpeg is detected on the system.
9
+ * Falls back to the WASM encoder (in @bendyline/squisq-video) when unavailable.
10
+ */
11
+ import type { VideoExportOptions } from '@bendyline/squisq-video';
12
+ /**
13
+ * Encode frame PNGs to MP4 using native ffmpeg.
14
+ *
15
+ * @param ffmpegPath - Absolute path to the ffmpeg binary
16
+ * @param frames - Array of PNG image bytes (one per frame)
17
+ * @param audio - Optional audio file bytes to mux
18
+ * @param outputPath - Where to write the final MP4
19
+ * @param options - Encoding options (fps, quality, dimensions, progress)
20
+ */
21
+ export declare function framesToMp4Native(ffmpegPath: string, frames: Uint8Array[], audio: Uint8Array | null, outputPath: string, options?: VideoExportOptions): Promise<void>;
22
+ /**
23
+ * Encode frames using native ffmpeg and return the MP4 bytes (instead of writing to disk).
24
+ * Useful when the caller needs the bytes in memory.
25
+ */
26
+ export declare function framesToMp4NativeBytes(ffmpegPath: string, frames: Uint8Array[], audio: Uint8Array | null, options?: VideoExportOptions): Promise<Uint8Array>;
27
+ //# sourceMappingURL=nativeEncoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeEncoder.d.ts","sourceRoot":"","sources":["../../src/util/nativeEncoder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGlE;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,UAAU,EAAE,EACpB,KAAK,EAAE,UAAU,GAAG,IAAI,EACxB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAuFf;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,UAAU,EAAE,EACpB,KAAK,EAAE,UAAU,GAAG,IAAI,EACxB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,UAAU,CAAC,CAWrB"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Native FFmpeg Encoder
3
+ *
4
+ * Encodes PNG frames to MP4 using a locally installed ffmpeg binary.
5
+ * Writes frames to a temporary directory, invokes ffmpeg as a child process,
6
+ * and reads the resulting MP4.
7
+ *
8
+ * This is the fast path — used when native ffmpeg is detected on the system.
9
+ * Falls back to the WASM encoder (in @bendyline/squisq-video) when unavailable.
10
+ */
11
+ import { execFile } from 'node:child_process';
12
+ import { writeFile, readFile, mkdir, rm } from 'node:fs/promises';
13
+ import { join } from 'node:path';
14
+ import { tmpdir } from 'node:os';
15
+ import { randomBytes } from 'node:crypto';
16
+ import { QUALITY_PRESETS, resolveDimensions } from '@bendyline/squisq-video';
17
+ /**
18
+ * Encode frame PNGs to MP4 using native ffmpeg.
19
+ *
20
+ * @param ffmpegPath - Absolute path to the ffmpeg binary
21
+ * @param frames - Array of PNG image bytes (one per frame)
22
+ * @param audio - Optional audio file bytes to mux
23
+ * @param outputPath - Where to write the final MP4
24
+ * @param options - Encoding options (fps, quality, dimensions, progress)
25
+ */
26
+ export async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
27
+ const fps = options.fps ?? 30;
28
+ const quality = options.quality ?? 'normal';
29
+ const { width, height } = resolveDimensions(options);
30
+ const preset = QUALITY_PRESETS[quality];
31
+ const onProgress = options.onProgress;
32
+ if (frames.length === 0) {
33
+ throw new Error('No frames provided for encoding');
34
+ }
35
+ // Create a temp working directory
36
+ const tmpId = randomBytes(8).toString('hex');
37
+ const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
38
+ await mkdir(workDir, { recursive: true });
39
+ try {
40
+ onProgress?.(0, 'writing frames');
41
+ // Write frame PNGs to temp directory
42
+ const padLen = String(frames.length).length;
43
+ for (let i = 0; i < frames.length; i++) {
44
+ const name = `frame-${String(i + 1).padStart(padLen, '0')}.png`;
45
+ await writeFile(join(workDir, name), frames[i]);
46
+ if (onProgress && i % 10 === 0) {
47
+ onProgress(Math.round((i / frames.length) * 30), 'writing frames');
48
+ }
49
+ }
50
+ // Write audio if provided
51
+ let audioPath = null;
52
+ if (audio) {
53
+ audioPath = join(workDir, 'audio-input');
54
+ await writeFile(audioPath, audio);
55
+ }
56
+ onProgress?.(30, 'encoding');
57
+ // Build ffmpeg arguments
58
+ const padPattern = join(workDir, `frame-%0${padLen}d.png`);
59
+ const args = ['-y', '-framerate', String(fps), '-i', padPattern];
60
+ if (audioPath) {
61
+ args.push('-i', audioPath);
62
+ }
63
+ args.push('-c:v', 'libx264', '-preset', preset.preset, '-crf', String(preset.crf), '-pix_fmt', 'yuv420p', '-vf', `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`);
64
+ if (audioPath) {
65
+ args.push('-c:a', 'aac', '-b:a', '128k', '-shortest');
66
+ }
67
+ args.push(outputPath);
68
+ // Run ffmpeg
69
+ await new Promise((resolve, reject) => {
70
+ const proc = execFile(ffmpegPath, args, { timeout: 600000 }, (err) => {
71
+ if (err) {
72
+ reject(new Error(`ffmpeg failed: ${err.message}`));
73
+ }
74
+ else {
75
+ resolve();
76
+ }
77
+ });
78
+ // ffmpeg writes progress to stderr
79
+ proc.stderr?.on('data', () => {
80
+ // Could parse ffmpeg progress output here in the future
81
+ });
82
+ });
83
+ onProgress?.(100, 'done');
84
+ }
85
+ finally {
86
+ // Clean up temp directory
87
+ await rm(workDir, { recursive: true, force: true });
88
+ }
89
+ }
90
+ /**
91
+ * Encode frames using native ffmpeg and return the MP4 bytes (instead of writing to disk).
92
+ * Useful when the caller needs the bytes in memory.
93
+ */
94
+ export async function framesToMp4NativeBytes(ffmpegPath, frames, audio, options = {}) {
95
+ const tmpId = randomBytes(8).toString('hex');
96
+ const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);
97
+ try {
98
+ await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);
99
+ const data = await readFile(tmpOutput);
100
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
101
+ }
102
+ finally {
103
+ await rm(tmpOutput, { force: true });
104
+ }
105
+ }
106
+ //# sourceMappingURL=nativeEncoder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeEncoder.js","sourceRoot":"","sources":["../../src/util/nativeEncoder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * readInput
3
+ *
4
+ * Unified input reader for the CLI. Accepts a path to a .md file, .json file
5
+ * (Doc schema), .zip/.dbk container, or a folder and returns a MemoryContentContainer
6
+ * (virtual file system) plus either the parsed MarkdownDocument or a pre-built Doc.
7
+ *
8
+ * When a .json file or a container/folder containing doc.json is provided, the
9
+ * Doc is returned directly and markdownDoc will be null. Callers should check
10
+ * result.doc first; when present, skip markdownToDoc() and use it directly.
11
+ */
12
+ import type { MarkdownDocument } from '@bendyline/squisq/markdown';
13
+ import type { Doc } from '@bendyline/squisq/schemas';
14
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
15
+ export interface ReadInputResult {
16
+ container: MemoryContentContainer;
17
+ /** Parsed markdown document. Null when input is a Doc JSON file. */
18
+ markdownDoc: MarkdownDocument | null;
19
+ /** Pre-built Doc from JSON input. Present when input is .json or contains doc.json. */
20
+ doc?: Doc;
21
+ }
22
+ /**
23
+ * Read input from a file path (markdown, JSON Doc, ZIP/DBK container, or folder)
24
+ * and return a populated ContentContainer + parsed MarkdownDocument or Doc.
25
+ *
26
+ * When the input is a .json file (or a container/folder containing doc.json),
27
+ * the result will have `doc` populated and `markdownDoc` set to null.
28
+ * Otherwise, `markdownDoc` is populated and `doc` is undefined.
29
+ */
30
+ export declare function readInput(inputPath: string): Promise<ReadInputResult>;
31
+ //# sourceMappingURL=readInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readInput.d.ts","sourceRoot":"","sources":["../../src/util/readInput.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAGnE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,sBAAsB,CAAC;IAClC,oEAAoE;IACpE,WAAW,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACrC,uFAAuF;IACvF,GAAG,CAAC,EAAE,GAAG,CAAC;CACX;AA2CD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAkB3E"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * readInput
3
+ *
4
+ * Unified input reader for the CLI. Accepts a path to a .md file, .json file
5
+ * (Doc schema), .zip/.dbk container, or a folder and returns a MemoryContentContainer
6
+ * (virtual file system) plus either the parsed MarkdownDocument or a pre-built Doc.
7
+ *
8
+ * When a .json file or a container/folder containing doc.json is provided, the
9
+ * Doc is returned directly and markdownDoc will be null. Callers should check
10
+ * result.doc first; when present, skip markdownToDoc() and use it directly.
11
+ */
12
+ import { readFile, readdir, stat } from 'node:fs/promises';
13
+ import { join, extname } from 'node:path';
14
+ import { parseMarkdown } from '@bendyline/squisq/markdown';
15
+ import { MemoryContentContainer } from '@bendyline/squisq/storage';
16
+ import { zipToContainer } from '@bendyline/squisq-formats/container';
17
+ /** MIME type lookup by extension (common content types) */
18
+ const MIME_TYPES = {
19
+ '.md': 'text/markdown',
20
+ '.txt': 'text/plain',
21
+ '.json': 'application/json',
22
+ '.jpg': 'image/jpeg',
23
+ '.jpeg': 'image/jpeg',
24
+ '.png': 'image/png',
25
+ '.gif': 'image/gif',
26
+ '.webp': 'image/webp',
27
+ '.svg': 'image/svg+xml',
28
+ '.mp3': 'audio/mpeg',
29
+ '.wav': 'audio/wav',
30
+ '.ogg': 'audio/ogg',
31
+ '.mp4': 'video/mp4',
32
+ '.webm': 'video/webm',
33
+ };
34
+ function mimeFromExt(filePath) {
35
+ return MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
36
+ }
37
+ /**
38
+ * Recursively walk a directory and return all file paths (relative to root).
39
+ */
40
+ async function walkDir(root, prefix = '') {
41
+ const entries = await readdir(root, { withFileTypes: true });
42
+ const paths = [];
43
+ for (const entry of entries) {
44
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
45
+ if (entry.isDirectory()) {
46
+ paths.push(...(await walkDir(join(root, entry.name), relPath)));
47
+ }
48
+ else if (entry.isFile()) {
49
+ paths.push(relPath);
50
+ }
51
+ }
52
+ return paths;
53
+ }
54
+ /**
55
+ * Read input from a file path (markdown, JSON Doc, ZIP/DBK container, or folder)
56
+ * and return a populated ContentContainer + parsed MarkdownDocument or Doc.
57
+ *
58
+ * When the input is a .json file (or a container/folder containing doc.json),
59
+ * the result will have `doc` populated and `markdownDoc` set to null.
60
+ * Otherwise, `markdownDoc` is populated and `doc` is undefined.
61
+ */
62
+ export async function readInput(inputPath) {
63
+ const info = await stat(inputPath);
64
+ if (info.isDirectory()) {
65
+ return readFolder(inputPath);
66
+ }
67
+ const ext = extname(inputPath).toLowerCase();
68
+ if (ext === '.zip' || ext === '.dbk') {
69
+ return readContainer(inputPath);
70
+ }
71
+ if (ext === '.json') {
72
+ return readDocJsonFile(inputPath);
73
+ }
74
+ // Default: treat as a markdown file
75
+ return readMarkdownFile(inputPath);
76
+ }
77
+ async function readMarkdownFile(filePath) {
78
+ const content = await readFile(filePath, 'utf-8');
79
+ const container = new MemoryContentContainer();
80
+ await container.writeDocument(content);
81
+ const markdownDoc = parseMarkdown(content);
82
+ return { container, markdownDoc };
83
+ }
84
+ /**
85
+ * Read a standalone Doc JSON file. The container is empty (no media bundled);
86
+ * callers should populate it or set basePath for media resolution.
87
+ */
88
+ async function readDocJsonFile(filePath) {
89
+ const content = await readFile(filePath, 'utf-8');
90
+ const doc = JSON.parse(content);
91
+ const container = new MemoryContentContainer();
92
+ return { container, markdownDoc: null, doc };
93
+ }
94
+ /**
95
+ * Known filenames for Doc JSON inside containers and folders.
96
+ * Checked in priority order before falling back to markdown discovery.
97
+ */
98
+ const DOC_JSON_NAMES = ['doc.json', 'story.json'];
99
+ async function readContainer(filePath) {
100
+ const data = await readFile(filePath);
101
+ const container = await zipToContainer(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
102
+ // Check for Doc JSON inside the container first
103
+ for (const name of DOC_JSON_NAMES) {
104
+ const jsonData = await container.readFile(name);
105
+ if (jsonData) {
106
+ const decoder = new TextDecoder();
107
+ const doc = JSON.parse(decoder.decode(jsonData));
108
+ return { container, markdownDoc: null, doc };
109
+ }
110
+ }
111
+ const markdown = await container.readDocument();
112
+ if (!markdown) {
113
+ throw new Error(`No markdown document or doc.json found in container: ${filePath}`);
114
+ }
115
+ const markdownDoc = parseMarkdown(markdown);
116
+ return { container, markdownDoc };
117
+ }
118
+ async function readFolder(dirPath) {
119
+ const container = new MemoryContentContainer();
120
+ const files = await walkDir(dirPath);
121
+ for (const relPath of files) {
122
+ const absPath = join(dirPath, relPath);
123
+ const data = await readFile(absPath);
124
+ await container.writeFile(relPath, new Uint8Array(data.buffer, data.byteOffset, data.byteLength), mimeFromExt(relPath));
125
+ }
126
+ // Check for Doc JSON inside the folder first
127
+ for (const name of DOC_JSON_NAMES) {
128
+ const jsonData = await container.readFile(name);
129
+ if (jsonData) {
130
+ const decoder = new TextDecoder();
131
+ const doc = JSON.parse(decoder.decode(jsonData));
132
+ return { container, markdownDoc: null, doc };
133
+ }
134
+ }
135
+ const markdown = await container.readDocument();
136
+ if (!markdown) {
137
+ throw new Error(`No markdown document or doc.json found in folder: ${dirPath}`);
138
+ }
139
+ const markdownDoc = parseMarkdown(markdown);
140
+ return { container, markdownDoc };
141
+ }
142
+ //# sourceMappingURL=readInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readInput.js","sourceRoot":"","sources":["../../src/util/readInput.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Squisq CLI — convert markdown documents to DOCX, PDF, HTML, and more",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -28,6 +28,18 @@
28
28
  },
29
29
  "main": "./dist/index.js",
30
30
  "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "./api": {
38
+ "types": "./dist/api.d.ts",
39
+ "import": "./dist/api.js",
40
+ "default": "./dist/api.js"
41
+ }
42
+ },
31
43
  "files": [
32
44
  "dist"
33
45
  ],
@@ -37,10 +49,10 @@
37
49
  "test": "mocha"
38
50
  },
39
51
  "dependencies": {
40
- "@bendyline/squisq": "1.1.0",
41
- "@bendyline/squisq-formats": "1.1.0",
42
- "@bendyline/squisq-react": "1.0.2",
43
- "@bendyline/squisq-video": "1.0.0",
52
+ "@bendyline/squisq": "1.2.0",
53
+ "@bendyline/squisq-formats": "1.2.0",
54
+ "@bendyline/squisq-react": "1.1.0",
55
+ "@bendyline/squisq-video": "1.0.2",
44
56
  "commander": "^12.0.0",
45
57
  "playwright-core": "^1.48.0"
46
58
  },