@bendyline/squisq-cli 1.2.1 → 2.0.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.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/commands/convert.ts","../src/util/domPolyfill.ts","../src/util/readInput.ts","../src/registry.ts","../src/api.ts","../src/util/detectFfmpeg.ts","../src/util/audioMix.ts","../src/util/applyTransformPipeline.ts","../src/commands/video.ts","../src/commands/validate.ts","../src/commands/doctor.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 { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { registerConvertCommand } from './commands/convert.js';\nimport { registerVideoCommand } from './commands/video.js';\nimport { registerValidateCommand } from './commands/validate.js';\nimport { registerDoctorCommand } from './commands/doctor.js';\n\n// Read the real version from package.json at runtime (ESM-safe). From the built\n// `dist/index.js`, `../package.json` resolves to `packages/cli/package.json`.\nconst require = createRequire(import.meta.url);\nconst { version } = require('../package.json') as { version: string };\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[33mv${version}\\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(version);\n\nregisterConvertCommand(program);\nregisterVideoCommand(program);\nregisterValidateCommand(program);\nregisterDoctorCommand(program);\n\nprogram.parse();\n","/**\n * convert command\n *\n * Reads any supported input — a markdown file, a JSON Doc, a `.zip`/`.dbk`\n * container, a folder, or an importable binary (`.docx`/`.pptx`/`.pdf`/\n * `.xlsx`/`.csv`/`.html`) — and exports it to one or more target formats via\n * the shared format registry's `convert()`.\n *\n * Supports optional --theme and --transform flags to apply a squisq theme\n * and/or transform style before exporting.\n *\n * Usage:\n * squisq convert <input> [--output-dir <dir>] [--formats <list>] [--theme <id>] [--transform <style>]\n * squisq convert <input> -o <file> # single output; format inferred from extension\n * squisq convert <input> --format <id> # single format to the default output dir\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { dirname, basename, extname, join, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport { BUILTIN_FORMAT_IDS, ConversionError } from '@bendyline/squisq-formats';\nimport type { ConvertSource, FormatId } from '@bendyline/squisq-formats';\nimport { readInput } from '../util/readInput.js';\nimport { createCliRegistry } from '../registry.js';\nimport { convert } from '../api.js';\nimport { assertValidThemeId, assertValidTransformStyle } from '../util/applyTransformPipeline.js';\n\n/** Every format the registry can export (built-ins + the CLI-only mp4). */\nconst VALID_FORMATS: readonly FormatId[] = [...BUILTIN_FORMAT_IDS, 'mp4'];\n\n/**\n * Default formats produced by a bare `convert <input>` (no -o / --format /\n * --formats). Deliberately excludes md/xlsx/csv and — crucially — mp4, so a\n * bare convert never spins up Playwright/FFmpeg.\n */\nconst DEFAULT_FORMATS: readonly FormatId[] = [\n 'docx',\n 'pptx',\n 'pdf',\n 'html',\n 'htmlzip',\n 'epub',\n 'dbk',\n];\n\nfunction isValidFormat(id: string): boolean {\n return VALID_FORMATS.includes(id);\n}\n\nfunction parseFormats(value: string): FormatId[] {\n const requested = value.split(',').map((s) => s.trim().toLowerCase());\n const valid: FormatId[] = [];\n for (const r of requested) {\n if (isValidFormat(r)) {\n valid.push(r);\n } else {\n console.warn(`Unknown format \"${r}\" — skipping. Valid: ${VALID_FORMATS.join(', ')}`);\n }\n }\n if (valid.length === 0) {\n throw new Error(`No valid formats specified. Valid: ${VALID_FORMATS.join(', ')}`);\n }\n return valid;\n}\n\n/** Infer a target format id from an explicit output file path's extension. */\nfunction formatFromOutputPath(outputPath: string): FormatId {\n const lower = outputPath.toLowerCase();\n if (lower.endsWith('.html.zip')) return 'htmlzip';\n const ext = extname(lower);\n const def = createCliRegistry().byExtension(ext);\n if (!def) {\n throw new Error(\n `Cannot infer a format from output extension \"${ext || '(none)'}\". ` +\n `Use --format to specify one. Valid: ${VALID_FORMATS.join(', ')}`,\n );\n }\n return def.id;\n}\n\ninterface ConvertOpts {\n output?: string;\n outputDir?: string;\n formats?: string;\n format?: string;\n theme?: string;\n transform?: string;\n autoTemplates?: boolean;\n}\n\nexport function registerConvertCommand(program: Command): void {\n program\n .command('convert')\n .description('Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, and container formats')\n .argument('<input>', 'Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder')\n .option('-o, --output <file>', 'Single output file (format inferred from its extension)')\n .option(\n '-d, --output-dir <dir>',\n 'Output directory for multi-format export (default: same as input)',\n )\n .option(\n '-f, --formats <list>',\n `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(', ')}`,\n )\n .option('--format <id>', 'Produce a single format (alias for a one-entry --formats)')\n .option('-t, --theme <id>', 'Squisq theme ID to apply (e.g., documentary, cinematic, bold)')\n .option(\n '--transform <style>',\n 'Transform style to apply before export (e.g., documentary, magazine, minimal)',\n )\n .option(\n '--no-auto-templates',\n 'Disable content-aware template auto-picking for unannotated headings',\n )\n .action(async (inputPath: string, opts: ConvertOpts) => {\n try {\n await runConvert(inputPath, opts);\n } catch (err: unknown) {\n if (err instanceof ConversionError) {\n console.error(`Error: ${err.message}`);\n if (err.hint) console.error(` ${err.hint}`);\n } else {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n }\n process.exitCode = 1;\n }\n });\n}\n\n/** Resolve the set of target formats from the mutually-exclusive flags. */\nfunction resolveFormats(opts: ConvertOpts): FormatId[] {\n const hasOutput = Boolean(opts.output);\n const hasFormatSelection = Boolean(opts.formats || opts.format);\n\n if (hasOutput && hasFormatSelection) {\n throw new Error(\n '--output is a single-file destination and cannot be combined with --formats/--format.',\n );\n }\n\n if (hasOutput) {\n return [formatFromOutputPath(opts.output!)];\n }\n if (opts.format) {\n if (!isValidFormat(opts.format.toLowerCase())) {\n throw new Error(`Unknown format \"${opts.format}\". Valid: ${VALID_FORMATS.join(', ')}`);\n }\n return [opts.format.toLowerCase()];\n }\n if (opts.formats) {\n return parseFormats(opts.formats);\n }\n return [...DEFAULT_FORMATS];\n}\n\nasync function runConvert(inputPath: string, opts: ConvertOpts): Promise<void> {\n const resolvedInput = resolve(inputPath);\n\n const formats = resolveFormats(opts);\n\n // Base filename (used for suggested output names in multi-format mode).\n const inputBasename = basename(resolvedInput);\n const inputExt = extname(inputBasename);\n const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;\n\n // Validate the transform id up front (the style registry is static). The\n // theme id is validated after the doc is read, so a custom theme inlined in\n // the doc's frontmatter is admitted rather than rejected as \"unknown\".\n if (opts.transform) {\n await assertValidTransformStyle(opts.transform);\n }\n\n console.error(`Reading: ${resolvedInput}`);\n const result = await readInput(resolvedInput);\n\n if (opts.theme) {\n await assertValidThemeId(opts.theme, result);\n }\n\n // Build the convert() source once: prefer the markdown shape (so auto-\n // templating options apply), else the pre-built Doc.\n const source: ConvertSource = result.markdownDoc\n ? {\n kind: 'markdown',\n markdown: result.markdownDoc,\n container: result.container,\n baseName,\n }\n : { kind: 'doc', doc: result.doc, container: result.container, baseName };\n\n if (opts.transform) {\n console.error(` Applying transform: ${opts.transform}`);\n }\n\n for (const format of formats) {\n const conversion = await convert(source, format, {\n themeId: opts.theme,\n transformStyle: opts.transform,\n autoTemplates: opts.autoTemplates,\n title: baseName,\n });\n\n for (const warning of conversion.warnings) {\n console.error(` ⚠ ${warning}`);\n }\n\n const outPath = opts.output\n ? resolve(opts.output)\n : join(\n opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput),\n conversion.suggestedFilename,\n );\n\n await mkdir(dirname(outPath), { recursive: true });\n await writeFile(outPath, Buffer.from(conversion.bytes));\n console.error(` ✓ ${outPath}`);\n }\n\n console.error('Done.');\n}\n","/**\n * DOMParser polyfill for Node.\n *\n * The formats package is browser-pure: its shared OOXML reader\n * (`ooxml/reader.ts`, used by DOCX/PPTX/XLSX import) parses XML with the global\n * `DOMParser`, which browsers and jsdom provide but bare Node does not. The CLI\n * runs in Node, so importing this module for its side effect installs a\n * `@xmldom/xmldom`-backed `DOMParser` on `globalThis` when one is missing.\n *\n * The install is idempotent and gated on absence, so it is a no-op in a browser\n * or once the formats package no longer needs a global parser.\n */\n\nimport { DOMParser as XmldomDOMParser } from '@xmldom/xmldom';\n\nconst globalScope = globalThis as { DOMParser?: unknown };\n\nif (typeof globalScope.DOMParser === 'undefined') {\n // @xmldom/xmldom's DOMParser implements the DOM Level 2 surface the OOXML\n // reader relies on (getElementsByTagName[NS], getAttribute, localName,\n // textContent). Its constructor signature is compatible with the browser's.\n globalScope.DOMParser = XmldomDOMParser as unknown as typeof DOMParser;\n}\n","/**\n * readInput\n *\n * Unified input reader for the CLI. Accepts a path to a markdown file, a JSON\n * Doc file, a `.zip`/`.dbk` container, a folder, or any importable binary\n * format (`.docx`/`.pptx`/`.pdf`/`.xlsx`/`.csv`/`.html`/`.htm`) and always\n * resolves it to a fully-populated {@link ReadInputResult}: a `Doc`, a\n * `ContentContainer` holding the document + any bundled media, the parsed\n * `MarkdownDocument` when the source was markdown-shaped, and the detected\n * `sourceFormat`.\n *\n * Binary formats are dispatched through the shared format registry's importer\n * (preferring `importContainer`, falling back to `importDoc`); every path\n * derives the `Doc` via `markdownToDoc()` when the source is markdown-shaped.\n */\n\nimport './domPolyfill.js';\nimport { readFile, readdir, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\nimport { parseMarkdown, stringifyMarkdown } from '@bendyline/squisq/markdown';\nimport type { MarkdownDocument } from '@bendyline/squisq/markdown';\nimport { markdownToDoc } from '@bendyline/squisq/doc';\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport { MemoryContentContainer } from '@bendyline/squisq/storage';\nimport { zipToContainer } from '@bendyline/squisq-formats/container';\nimport { defaultRegistry } from '@bendyline/squisq-formats';\nimport type { FormatId } from '@bendyline/squisq-formats';\n\nexport interface ReadInputResult {\n /** The resolved document. Always present. */\n doc: Doc;\n /** Virtual file system holding the document plus any bundled media. */\n container: ContentContainer;\n /**\n * The parsed markdown document. Present when the source was markdown-shaped\n * (`.md`, container/folder markdown, or a binary format imported as\n * markdown); absent for JSON-Doc input.\n */\n markdownDoc?: MarkdownDocument;\n /** Detected source format id (registry id, or `md`/`json`/`dbk`/`folder`). */\n sourceFormat: FormatId;\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\n/**\n * Binary extensions dispatched through the registry importer. `.md`, `.zip`,\n * `.dbk`, and `.json` are intentionally excluded — they have dedicated paths\n * that also detect bundled media / doc.json.\n */\nconst IMPORTER_EXTS = ['.docx', '.pptx', '.pdf', '.xlsx', '.csv', '.html', '.htm'];\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, JSON Doc, ZIP/DBK container, folder,\n * or an importable binary format) and resolve it to a populated\n * {@link ReadInputResult}.\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 if (ext === '.json') {\n return readDocJsonFile(inputPath);\n }\n\n if (IMPORTER_EXTS.includes(ext)) {\n const def = defaultRegistry().byExtension(ext);\n if (def && (def.importContainer || def.importDoc)) {\n return readViaImporter(inputPath, def);\n }\n }\n\n // Default: treat as a markdown file\n return readMarkdownFile(inputPath);\n}\n\n/** Read a file into an ArrayBuffer (a fresh copy, not a Buffer view). */\nasync function readArrayBuffer(filePath: string): Promise<ArrayBuffer> {\n const data = await readFile(filePath);\n return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;\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 { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: 'md' };\n}\n\n/**\n * Read a standalone Doc JSON file. The container is empty (no media bundled);\n * callers should populate it or set basePath for media resolution.\n */\nasync function readDocJsonFile(filePath: string): Promise<ReadInputResult> {\n const content = await readFile(filePath, 'utf-8');\n const doc = JSON.parse(content) as Doc;\n const container = new MemoryContentContainer();\n return { doc, container, sourceFormat: 'json' };\n}\n\n/**\n * Dispatch an importable binary format (docx/pptx/pdf/…) through the registry\n * importer. Prefers `importContainer` (extracts media alongside the markdown),\n * falling back to `importDoc` wrapped in a fresh container.\n */\nasync function readViaImporter(\n filePath: string,\n def: import('@bendyline/squisq-formats').FormatDefinition,\n): Promise<ReadInputResult> {\n const buffer = await readArrayBuffer(filePath);\n\n let container: ContentContainer;\n let markdownDoc: MarkdownDocument;\n if (def.importContainer) {\n container = await def.importContainer(buffer, {});\n const text = await container.readDocument();\n markdownDoc = text ? parseMarkdown(text) : { type: 'document', children: [] };\n } else {\n // importDoc is guaranteed present by the caller's guard.\n markdownDoc = await def.importDoc!(buffer, {});\n const mem = new MemoryContentContainer();\n await mem.writeDocument(stringifyMarkdown(markdownDoc));\n container = mem;\n }\n\n return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat: def.id };\n}\n\n/**\n * Known filenames for Doc JSON inside containers and folders.\n * Checked in priority order before falling back to markdown discovery.\n */\nconst DOC_JSON_NAMES = ['doc.json', 'story.json'];\n\n/** Resolve a container that may hold either a doc.json or a markdown document. */\nasync function resolveContainer(\n container: ContentContainer,\n sourceFormat: FormatId,\n missingMessage: string,\n): Promise<ReadInputResult> {\n // Check for Doc JSON first.\n for (const name of DOC_JSON_NAMES) {\n const jsonData = await container.readFile(name);\n if (jsonData) {\n const doc = JSON.parse(new TextDecoder().decode(jsonData)) as Doc;\n return { doc, container, sourceFormat };\n }\n }\n\n const markdown = await container.readDocument();\n if (!markdown) {\n throw new Error(missingMessage);\n }\n\n const markdownDoc = parseMarkdown(markdown);\n return { doc: markdownToDoc(markdownDoc), container, markdownDoc, sourceFormat };\n}\n\nasync function readContainer(filePath: string): Promise<ReadInputResult> {\n const container = await zipToContainer(await readArrayBuffer(filePath));\n return resolveContainer(\n container,\n 'dbk',\n `No markdown document or doc.json found in container: ${filePath}`,\n );\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 return resolveContainer(\n container,\n 'folder',\n `No markdown document or doc.json found in folder: ${dirPath}`,\n );\n}\n","/**\n * CLI format registry.\n *\n * The CLI extends the formats package's default registry with an `mp4` format\n * so `convert()` can produce video the same way it produces DOCX/PDF/HTML. MP4\n * export is Node-only (it needs Playwright + FFmpeg), which is why it lives\n * here in the CLI rather than in the browser-pure formats package.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { readFile, rm } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { defaultRegistry } from '@bendyline/squisq-formats';\nimport type {\n ConversionResult,\n ConvertOptions,\n FormatDefinition,\n FormatRegistry,\n NormalizedInput,\n} from '@bendyline/squisq-formats';\nimport type { VideoOrientation, VideoQuality } from '@bendyline/squisq-video';\nimport { renderDocToMp4 } from './api.js';\n\n/** Sensible defaults for `convert(..., 'mp4')` — a full-quality landscape clip. */\nconst MP4_DEFAULTS = {\n fps: 30,\n quality: 'normal' as VideoQuality,\n orientation: 'landscape' as VideoOrientation,\n coverPreRoll: 0,\n} as const;\n\n/**\n * The `mp4` {@link FormatDefinition}.\n *\n * Export-only: it renders the normalized Doc to a temporary MP4 on disk via\n * {@link renderDocToMp4}, reads the produced file back into a `Uint8Array`, and\n * always deletes the temp file (even on failure). Per-render knobs (fps,\n * quality, orientation, coverPreRoll) can be supplied through\n * `options.formatOptions.mp4`; otherwise {@link MP4_DEFAULTS} apply.\n */\nfunction mp4Format(): FormatDefinition {\n return {\n id: 'mp4',\n label: 'MP4 Video',\n mimeType: 'video/mp4',\n extensions: ['.mp4'],\n async exportDoc(input: NormalizedInput, options: ConvertOptions): Promise<ConversionResult> {\n // `renderDocToMp4` is imported statically from api.ts. api.ts imports\n // `createCliRegistry` from this module, forming an ES module cycle — but\n // both are hoisted function exports used only at call time, so the cycle\n // resolves cleanly.\n const mp4Opts = options.formatOptions?.mp4 ?? {};\n const fps = typeof mp4Opts.fps === 'number' ? mp4Opts.fps : MP4_DEFAULTS.fps;\n const quality =\n typeof mp4Opts.quality === 'string'\n ? (mp4Opts.quality as VideoQuality)\n : MP4_DEFAULTS.quality;\n const orientation =\n typeof mp4Opts.orientation === 'string'\n ? (mp4Opts.orientation as VideoOrientation)\n : MP4_DEFAULTS.orientation;\n const coverPreRoll =\n typeof mp4Opts.coverPreRoll === 'number' ? mp4Opts.coverPreRoll : MP4_DEFAULTS.coverPreRoll;\n\n const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString('hex')}.mp4`);\n try {\n await renderDocToMp4(input.doc, input.container, {\n outputPath,\n fps,\n quality,\n orientation,\n coverPreRoll,\n });\n const data = await readFile(outputPath);\n const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n return { bytes, mimeType: 'video/mp4', suggestedFilename: '', warnings: [] };\n } finally {\n await rm(outputPath, { force: true });\n }\n },\n };\n}\n\n/**\n * Build the CLI's format registry: every built-in format from\n * `@bendyline/squisq-formats` plus the CLI-only `mp4` exporter.\n */\nexport function createCliRegistry(): FormatRegistry {\n const registry = defaultRegistry();\n registry.register(mp4Format());\n return registry;\n}\n","/**\n * Programmatic Video API\n *\n * Provides a library-style entry point for rendering Squisq documents to MP4\n * from Node.js callers (e.g., Qualla's pipeline). This avoids the need to shell\n * out to the `squisq video` CLI and gives callers full control over the Doc,\n * MemoryContentContainer, and encoding options.\n *\n * Orchestrates the full pipeline: Doc → render HTML → Playwright frame capture → FFmpeg encode.\n *\n * Usage:\n * import { renderDocToMp4 } from '@bendyline/squisq-cli/api';\n *\n * await renderDocToMp4(doc, container, {\n * outputPath: '/tmp/output.mp4',\n * fps: 30,\n * quality: 'normal',\n * orientation: 'landscape',\n * });\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport { resolveMediaSchedule } from '@bendyline/squisq/schemas';\nimport { flattenBlocks } from '@bendyline/squisq/doc';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\nimport { generateRenderHtml } from '@bendyline/squisq-video';\nimport { resolveDimensions } from '@bendyline/squisq-video';\nimport { convert as formatsConvert } from '@bendyline/squisq-formats';\nimport type {\n ConvertSource,\n ConvertOptions,\n ConversionResult,\n FormatId,\n} from '@bendyline/squisq-formats';\nimport { detectFfmpeg } from './util/detectFfmpeg.js';\nimport { buildMixedAudioTrack } from './util/audioMix.js';\nimport { createCliRegistry } from './registry.js';\n\n// Re-export utility types and functions callers may need\nexport type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\nexport { MemoryContentContainer } from '@bendyline/squisq/storage';\nexport { readInput } from './util/readInput.js';\nexport type { ReadInputResult } from './util/readInput.js';\n\n// ── Format registry / convert() surface ───────────────────────────\n// Re-export the CLI's format registry factory plus the registry types and the\n// structured error, so `@bendyline/squisq-cli/api` is a one-stop programmatic\n// front door for both video rendering and document conversion.\nexport { createCliRegistry } from './registry.js';\nexport { ConversionError } from '@bendyline/squisq-formats';\nexport type {\n ConvertSource,\n ConvertOptions,\n ConversionResult,\n FormatId,\n FormatRegistry,\n FormatDefinition,\n NormalizedInput,\n ConversionErrorCode,\n ConversionErrorOptions,\n} from '@bendyline/squisq-formats';\n\n/**\n * Convert a document to a target format using the CLI's format registry.\n *\n * This is a thin, pre-bound wrapper over `convert()` from\n * `@bendyline/squisq-formats`: it injects the CLI registry (which adds the\n * `mp4` format on top of every built-in exporter) and a default\n * `resolvePlayerScript` that lazily loads the standalone player IIFE bundle\n * (required for HTML/EPUB-style exports). Callers may override either via\n * `options`.\n *\n * @param source - A bytes / markdown / doc {@link ConvertSource}.\n * @param to - Target format id (`docx`, `pdf`, `pptx`, `html`, `mp4`, …).\n * @param options - Conversion options; `registry` and `resolvePlayerScript`\n * default to the CLI's values but can be overridden.\n * @returns The encoded bytes plus mime type, suggested filename, and warnings.\n * @throws {@link ConversionError} on any failure, with a stable `code`.\n */\nexport async function convert(\n source: ConvertSource,\n to: FormatId,\n options: ConvertOptions = {},\n): Promise<ConversionResult> {\n return formatsConvert(source, to, {\n registry: createCliRegistry(),\n resolvePlayerScript: () =>\n import('@bendyline/squisq-react/standalone-source').then((m) => m.PLAYER_BUNDLE),\n ...options,\n });\n}\n\n/** Options for renderDocToMp4. */\nexport interface RenderDocToMp4Options {\n /** Output file path for the MP4. */\n outputPath: string;\n\n /** Frames per second (default: 30). */\n fps?: number;\n\n /** Encoding quality preset (default: 'normal'). */\n quality?: VideoQuality;\n\n /** Video orientation (default: 'landscape'). */\n orientation?: VideoOrientation;\n\n /** Override video width in pixels. */\n width?: number;\n\n /** Override video height in pixels. */\n height?: number;\n\n /** Caption style to bake into the video (default: none). */\n captionStyle?: 'standard' | 'social';\n\n /**\n * Seconds of cover-slide pre-roll before the story starts (default: 0).\n *\n * Note: the `squisq video` CLI defaults its `--cover-preroll` flag to 2\n * seconds; this programmatic API deliberately defaults to 0 so library\n * callers get exactly the duration they ask for.\n */\n coverPreRoll?: number;\n\n /**\n * Progress callback. Called with a phase name and 0-100 percentage.\n */\n onProgress?: (phase: string, percent: number) => void;\n}\n\n/** Result returned by renderDocToMp4. */\nexport interface RenderDocToMp4Result {\n /** Duration of the rendered video in seconds (including pre-roll). */\n duration: number;\n\n /** Number of frames captured. */\n frameCount: number;\n\n /** Output file path. */\n outputPath: string;\n}\n\n/**\n * Render a Doc + media container to an MP4 video file.\n *\n * The container should contain audio and image files referenced by the Doc's\n * audio.segments[].src and block image paths. Files are embedded as base64\n * data URIs in a self-contained render HTML page.\n *\n * Requires:\n * - Playwright (chromium) — for headless frame capture\n * - FFmpeg — for video encoding (resolved from SQUISQ_FFMPEG, PATH, or an\n * optionally installed `ffmpeg-static` package — see detectFfmpeg)\n *\n * @param doc - The Doc structure to render\n * @param container - MemoryContentContainer with audio/image files\n * @param options - Rendering and encoding options\n * @returns Result with duration and frame count\n */\nexport async function renderDocToMp4(\n doc: Doc,\n container: ContentContainer,\n options: RenderDocToMp4Options,\n): Promise<RenderDocToMp4Result> {\n const {\n outputPath,\n fps = 30,\n quality = 'normal',\n orientation = 'landscape',\n captionStyle,\n coverPreRoll = 0,\n onProgress,\n } = options;\n\n const dimensions = resolveDimensions({\n orientation,\n width: options.width,\n height: options.height,\n });\n\n // Detect ffmpeg early — needed for audio concat and video encoding\n const ffmpegPath = await detectFfmpeg();\n if (!ffmpegPath) {\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 'Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG to an ffmpeg binary.',\n );\n }\n\n onProgress?.('collecting media', 0);\n\n // ── Collect images 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 for the render HTML ──────────────────\n // The player page loads narration so the doc's duration/timing resolves; the\n // captured frames themselves are silent (audio is reconstructed offline from\n // the timeline below). Keyed by both src and name so the inline provider\n // resolves either reference.\n const audio = new Map<string, ArrayBuffer>();\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 audio.set(seg.name, data);\n }\n }\n }\n\n // ── Collect timed-media clips + block video sources ─────────────\n // Every scheduled clip (block.media + doc.documentMedia) and every\n // template-produced VideoLayer needs its bytes embedded so the headless\n // page can load them with no network. They resolve through the inline\n // media provider's `images` map (which infers mp4/mp3 MIME by extension).\n const mediaSrcs = new Set<string>(resolveMediaSchedule(doc).map((c) => c.src));\n for (const block of flattenBlocks(doc.blocks)) {\n for (const layer of block.layers ?? []) {\n if (layer.type === 'video') mediaSrcs.add(layer.content.src);\n }\n }\n for (const src of mediaSrcs) {\n if (images.has(src)) continue;\n const data = await container.readFile(src);\n if (data) images.set(src, data);\n }\n\n onProgress?.('generating render HTML', 10);\n\n // ── Generate self-contained render HTML ─────────────────────────\n const { PLAYER_BUNDLE } = await import('@bendyline/squisq-react/standalone-source');\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 onProgress?.('launching browser', 15);\n\n // ── Playwright frame capture ────────────────────────────────────\n const { chromium } = await import('playwright-core');\n let browser: import('playwright-core').Browser;\n try {\n browser = await chromium.launch({ headless: true });\n } catch (err: unknown) {\n const detail = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n throw new Error(\n 'Playwright Chromium is not installed. Run: npx playwright install chromium\\n' +\n `(launch failed: ${detail})`,\n );\n }\n const page = await browser.newPage({\n viewport: { width: dimensions.width, height: dimensions.height },\n });\n\n const pageErrors: string[] = [];\n page.on('pageerror', (err) => pageErrors.push(err.message));\n\n await page.setContent(renderHtml, { waitUntil: 'load' });\n await page.waitForTimeout(500);\n\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(\n `The standalone player failed to boot in headless Chromium. ` +\n `Render API did not initialize within 15 seconds.${errorDetail}`,\n );\n }\n\n const docDuration: number = await page.evaluate(() => {\n return (window as unknown as { getDuration: () => number }).getDuration();\n });\n\n if (docDuration <= 0) {\n await browser.close();\n throw new Error('Document has zero duration — nothing to render');\n }\n\n const storyFrameCount = Math.ceil(docDuration * fps);\n const preRollFrameCount = Math.ceil(coverPreRoll * fps);\n const totalFrames = preRollFrameCount + storyFrameCount;\n const frames: Uint8Array[] = [];\n\n onProgress?.('capturing frames', 20);\n\n // Cover slide pre-roll (if requested)\n if (preRollFrameCount > 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 await page.evaluate(() => {\n (window as unknown as { showCover: () => void }).showCover();\n });\n await page.waitForTimeout(100);\n const coverFrame = new Uint8Array(await page.screenshot({ type: 'png' }));\n for (let i = 0; i < preRollFrameCount; i++) {\n frames.push(coverFrame);\n }\n await page.evaluate(() => {\n (window as unknown as { hideCover: () => void }).hideCover();\n });\n }\n }\n\n // Story frames via seekTo\n const frameInterval = 1 / fps;\n for (let i = 0; i < storyFrameCount; i++) {\n const time = i * frameInterval;\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 // Report progress: frames phase is 20% to 80%\n if (i % Math.max(1, Math.floor(fps / 2)) === 0 || i === storyFrameCount - 1) {\n const pct = 20 + Math.round((frames.length / totalFrames) * 60);\n onProgress?.('capturing frames', pct);\n }\n }\n\n await browser.close();\n\n onProgress?.('encoding video', 80);\n\n // Build the final audio track from the single-source-of-truth timeline:\n // narration segments laid sequentially + timed media clips at their absolute\n // positions, every start shifted by the cover pre-roll. `buildMixedAudioTrack`\n // is a pure consumer of `computeAudioTimeline`, so the CLI's placement can no\n // longer drift from the browser export (zero-duration narration segments are\n // skipped consistently in both).\n const encodingAudio = await buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll);\n\n const { framesToMp4Native } = await import('./util/nativeEncoder.js');\n await framesToMp4Native(ffmpegPath, frames, encodingAudio, outputPath, {\n fps,\n quality,\n orientation,\n width: dimensions.width,\n height: dimensions.height,\n onProgress: (percent, phase) => {\n onProgress?.(`encoding: ${phase}`, 80 + Math.round(percent * 0.2));\n },\n });\n\n onProgress?.('done', 100);\n\n const totalDuration = docDuration + coverPreRoll;\n return {\n duration: totalDuration,\n frameCount: frames.length,\n outputPath,\n };\n}\n\n// ── Thumbnail extraction ──────────────────────────────────────────\n\n/** A thumbnail size specification. */\nexport interface ThumbnailSpec {\n /** Label for the thumbnail (used in filename: `{slug}-{width}x{height}.jpg`). */\n name: string;\n /** Output width in pixels. */\n width: number;\n /** Output height in pixels. */\n height: number;\n /** FFmpeg video filter string (e.g., 'scale=1280:720'). */\n filter: string;\n}\n\n/** Options for extractThumbnails. */\nexport interface ExtractThumbnailsOptions {\n /** Path to the source MP4 video. */\n videoPath: string;\n /** Directory to write thumbnails into. */\n outputDir: string;\n /** Base slug for filenames (produces `{slug}-{width}x{height}.jpg`). */\n slug: string;\n /** Thumbnail sizes to generate. */\n sizes: ThumbnailSpec[];\n /** Overwrite existing thumbnails (default: false). */\n force?: boolean;\n}\n\n/**\n * Extract thumbnail images from the first frame of an MP4 video.\n * Produces JPEG files at each specified size using FFmpeg video filters.\n */\nexport async function extractThumbnails(options: ExtractThumbnailsOptions): Promise<void> {\n const { videoPath, outputDir, slug, sizes, force } = options;\n const { existsSync } = await import('node:fs');\n const { join } = await import('node:path');\n const { execFile } = await import('node:child_process');\n\n const ffmpegPath = await detectFfmpeg();\n if (!ffmpegPath) {\n throw new Error(\n 'ffmpeg is required for thumbnail extraction 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 for (const thumb of sizes) {\n const outputPath = join(outputDir, `${slug}-${thumb.width}x${thumb.height}.jpg`);\n if (!force && existsSync(outputPath)) continue;\n\n await new Promise<void>((resolve, reject) => {\n execFile(\n ffmpegPath,\n ['-y', '-i', videoPath, '-vf', thumb.filter, '-frames:v', '1', '-q:v', '2', outputPath],\n { timeout: 30_000 },\n (err) => {\n if (err) reject(new Error(`Thumbnail extraction failed (${thumb.name}): ${err.message}`));\n else resolve();\n },\n );\n });\n }\n}\n","/**\n * detectFfmpeg\n *\n * Locates a native ffmpeg binary. Resolution order:\n * 1. `SQUISQ_FFMPEG` environment variable — verified by running\n * `<path> -version`. A value that is set but broken is a hard error\n * (never silently ignored), so misconfiguration surfaces immediately.\n * 2. System PATH lookup (`which` on macOS/Linux, `where` on Windows).\n * 3. The optional `ffmpeg-static` npm package, when the user has installed\n * it themselves. It is deliberately NOT a dependency of the CLI — the\n * import is opportunistic and falls through cleanly when absent.\n */\n\nimport { execFile } from 'node:child_process';\n\n/** Where a detected ffmpeg binary came from. */\nexport type FfmpegSource = 'env' | 'path' | 'ffmpeg-static';\n\n/** A successfully detected ffmpeg binary. */\nexport interface FfmpegDetection {\n /** Absolute path to the ffmpeg binary. */\n path: string;\n /** Which resolution step found it. */\n source: FfmpegSource;\n}\n\n/** Run a command and resolve with trimmed stdout, or null on any failure. */\nfunction run(command: string, args: string[]): Promise<string | null> {\n return new Promise((resolve) => {\n execFile(command, args, { timeout: 5000 }, (err, stdout) => {\n if (err || !stdout.trim()) {\n resolve(null);\n return;\n }\n resolve(stdout.trim());\n });\n });\n}\n\n/**\n * Run `<path> -version` and return the first line of output\n * (e.g. \"ffmpeg version 7.1 ...\"), or null when the binary fails to run.\n */\nexport async function getFfmpegVersion(path: string): Promise<string | null> {\n const out = await run(path, ['-version']);\n return out ? out.split('\\n')[0].trim() : null;\n}\n\n/**\n * Detect ffmpeg and report which source found it (see module doc for the\n * resolution order). Returns null when no working binary is found.\n *\n * @throws Error when `SQUISQ_FFMPEG` is set but the binary does not run —\n * an explicit override must never be silently ignored.\n */\nexport async function detectFfmpegDetailed(): Promise<FfmpegDetection | null> {\n // 1. Explicit override\n const envPath = process.env.SQUISQ_FFMPEG;\n if (envPath) {\n const version = await getFfmpegVersion(envPath);\n if (!version) {\n throw new Error(\n `SQUISQ_FFMPEG is set to \"${envPath}\" but running \"${envPath} -version\" failed. ` +\n 'Fix the path or unset SQUISQ_FFMPEG.',\n );\n }\n return { path: envPath, source: 'env' };\n }\n\n // 2. System PATH\n const command = process.platform === 'win32' ? 'where' : 'which';\n const found = await run(command, ['ffmpeg']);\n if (found) {\n // `which` and `where` can return multiple lines; take the first\n return { path: found.split('\\n')[0].trim(), source: 'path' };\n }\n\n // 3. Optional ffmpeg-static package. The specifier is a variable so\n // bundlers/TypeScript don't try to resolve a package we don't depend on.\n try {\n const specifier = 'ffmpeg-static';\n const mod = (await import(specifier)) as { default?: unknown } | string;\n // CJS default export shape (`module.exports = path`) surfaces as\n // `mod.default`; some build setups expose the string directly.\n const candidate =\n typeof mod === 'string' ? mod : typeof mod.default === 'string' ? mod.default : null;\n if (candidate) {\n return { path: candidate, source: 'ffmpeg-static' };\n }\n } catch {\n // Not installed — fall through.\n }\n\n return null;\n}\n\n/**\n * Detect whether native ffmpeg is available (SQUISQ_FFMPEG, PATH, or an\n * installed `ffmpeg-static` package).\n *\n * @returns Absolute path to ffmpeg binary, or null if not found\n * @throws Error when `SQUISQ_FFMPEG` is set but broken\n */\nexport async function detectFfmpeg(): Promise<string | null> {\n const detection = await detectFfmpegDetailed();\n return detection?.path ?? null;\n}\n","/**\n * audioMix — Build the MP4's final audio track from a doc's audio timeline.\n *\n * The schedule is produced by ONE source of truth: `computeAudioTimeline`\n * (from `@bendyline/squisq-video`), which flattens a doc's narration segments\n * (laid sequentially) and its timed media clips (placed at absolute positions\n * via `resolveMediaSchedule`) into a flat list of absolute-timed\n * `AudioTimelineClip`s. This CLI-side helper is a pure *consumer* of that\n * schedule: it loads each clip's bytes and assembles an ffmpeg `amix`\n * filtergraph where every clip is trimmed to its source window and delayed to\n * its absolute start.\n *\n * Because the browser MP4 export and this CLI mix now read the same timeline,\n * their audio placement can no longer drift. In particular, zero-duration\n * narration segments are skipped here exactly as they are in the browser\n * (`computeAudioTimeline` never emits a clip with `durationSec <= 0`).\n *\n * Frame capture stays silent — this reconstructs the audio offline from the\n * same schedule the renderer uses, keeping audio and video aligned.\n */\n\nimport type { Doc } from '@bendyline/squisq/schemas';\nimport type { ContentContainer } from '@bendyline/squisq/storage';\nimport { computeAudioTimeline } from '@bendyline/squisq-video';\nimport type { AudioTimelineClip } from '@bendyline/squisq-video';\n\n/**\n * Build the final mixed MP3 audio track for a doc's MP4 export.\n *\n * Drives the ffmpeg filtergraph entirely from `computeAudioTimeline(doc,\n * coverPreRoll)`: each clip's `startSec` → `adelay`, `sourceInSec` → `atrim`\n * start, `durationSec` → `atrim` duration, keyed by `src`. All clips are summed\n * with `amix` (`normalize=0` so individual levels are preserved).\n *\n * @param doc - The document whose audio to schedule.\n * @param container - Media container to read each clip's `src` bytes from.\n * @param ffmpegPath - Path to the ffmpeg binary.\n * @param coverPreRoll - Seconds of cover-slide pre-roll; shifts every clip's\n * start (matching the silent leading frames).\n * @returns The mixed MP3 bytes, or `null` when the doc has no audio to place.\n */\nexport async function buildMixedAudioTrack(\n doc: Doc,\n container: ContentContainer,\n ffmpegPath: string,\n coverPreRoll: number,\n): Promise<Uint8Array | null> {\n const timeline = computeAudioTimeline(doc, coverPreRoll);\n if (timeline.length === 0) return null;\n\n // Load each clip's bytes, caching reads by `src` (a src may back several clips).\n const bytesBySrc = new Map<string, ArrayBuffer | null>();\n const readSrc = async (src: string): Promise<ArrayBuffer | null> => {\n if (bytesBySrc.has(src)) return bytesBySrc.get(src)!;\n const data = await container.readFile(src);\n bytesBySrc.set(src, data ?? null);\n return data ?? null;\n };\n\n const usable: Array<{ clip: AudioTimelineClip; buffer: ArrayBuffer }> = [];\n for (const clip of timeline) {\n const buffer = await readSrc(clip.src);\n if (buffer) usable.push({ clip, buffer });\n }\n if (usable.length === 0) return null;\n\n return mixTimelineClips(ffmpegPath, usable);\n}\n\n/** Assemble the ffmpeg `amix` filtergraph and run it, returning the mixed MP3. */\nasync function mixTimelineClips(\n ffmpegPath: string,\n usable: Array<{ clip: AudioTimelineClip; buffer: ArrayBuffer }>,\n): Promise<Uint8Array> {\n const { writeFile, readFile, mkdir, rm } = await import('node:fs/promises');\n const { join } = await import('node:path');\n const { tmpdir } = await import('node:os');\n const { randomBytes } = await import('node:crypto');\n const { execFile } = await import('node:child_process');\n\n const workDir = join(tmpdir(), `squisq-audio-mix-${randomBytes(8).toString('hex')}`);\n await mkdir(workDir, { recursive: true });\n\n const ms = (s: number) => Math.max(0, Math.round(s * 1000));\n\n try {\n const inputs: string[] = [];\n const filters: string[] = [];\n const labels: string[] = [];\n\n for (const { clip, buffer } of usable) {\n const p = join(workDir, `clip-${inputs.length}.mp3`);\n await writeFile(p, new Uint8Array(buffer));\n const i = inputs.push(p) - 1;\n const delayMs = ms(clip.startSec);\n const start = Math.max(0, clip.sourceInSec);\n const end = start + Math.max(0, clip.durationSec);\n filters.push(\n `[${i}:a]atrim=start=${start}:end=${end},asetpts=PTS-STARTPTS,adelay=${delayMs}|${delayMs}[a${i}]`,\n );\n labels.push(`[a${i}]`);\n }\n\n const graph =\n `${filters.join(';')};${labels.join('')}` +\n `amix=inputs=${labels.length}:normalize=0:dropout_transition=0[aout]`;\n\n const outputPath = join(workDir, 'mixed.mp3');\n const args = ['-y'];\n for (const p of inputs) args.push('-i', p);\n args.push(\n '-filter_complex',\n graph,\n '-map',\n '[aout]',\n '-c:a',\n 'libmp3lame',\n '-b:a',\n '192k',\n outputPath,\n );\n\n await new Promise<void>((resolve, reject) => {\n execFile(ffmpegPath, args, { timeout: 180_000 }, (err) => {\n if (err) reject(new Error(`ffmpeg audio mix failed: ${err.message}`));\n else resolve();\n });\n });\n\n const data = await readFile(outputPath);\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n}\n","/**\n * applyTransformPipeline\n *\n * Shared `--theme` / `--transform` option handling for the convert and video\n * commands: option validation plus the transform application pipeline.\n *\n * Validation split (mirrors the original convert behavior):\n * - Transform styles can be validated up front — the style registry is static.\n * - Theme ids are validated only after the input has been read, so custom\n * themes inlined in the doc (frontmatter `squisq-custom-themes` or\n * `Doc.customThemes`) are admitted rather than rejected as \"unknown\".\n */\n\nimport type { MarkdownDocument } from '@bendyline/squisq/markdown';\nimport type { Doc } from '@bendyline/squisq/schemas';\n\n/**\n * Throw if `style` is not a registered transform style id.\n */\nexport async function assertValidTransformStyle(style: string): Promise<void> {\n const { getTransformStyleIds } = await import('@bendyline/squisq/transform');\n const styles = getTransformStyleIds();\n if (!styles.includes(style)) {\n throw new Error(`Unknown transform style \"${style}\". Available: ${styles.join(', ')}`);\n }\n}\n\n/**\n * Throw if `themeId` is neither a built-in theme nor a custom theme declared\n * by the document. Custom themes are read from the markdown frontmatter when\n * a MarkdownDocument is available, otherwise from `Doc.customThemes` (the\n * pre-built Doc JSON case).\n */\nexport async function assertValidThemeId(\n themeId: string,\n source: { markdownDoc?: MarkdownDocument | null; doc?: Doc | null },\n): Promise<void> {\n const { getAvailableThemes } = await import('@bendyline/squisq/schemas');\n const builtins = getAvailableThemes();\n\n let customIds: string[] = [];\n if (source.markdownDoc) {\n const { readCustomThemesFromFrontmatter } = await import('@bendyline/squisq/doc');\n customIds = (readCustomThemesFromFrontmatter(source.markdownDoc.frontmatter) ?? []).map(\n (t) => t.id,\n );\n } else if (source.doc?.customThemes) {\n customIds = source.doc.customThemes.map((t) => t.id);\n }\n\n if (!builtins.includes(themeId) && !customIds.includes(themeId)) {\n throw new Error(\n `Unknown theme \"${themeId}\". Available: ${[...builtins, ...customIds].join(', ')}`,\n );\n }\n}\n\n/**\n * Apply a transform style to a Doc.\n * Pipeline: Doc → extractDocImages → applyTransform → Doc\n */\nexport async function applyTransformToDoc(\n doc: Doc,\n transformStyle: string,\n themeId?: string,\n): Promise<Doc> {\n const { applyTransform, extractDocImages } = await import('@bendyline/squisq/transform');\n\n // Extract image metadata from the doc for transform interleaving\n const images = extractDocImages(doc.blocks);\n\n const result = applyTransform(doc, transformStyle, {\n themeId,\n images,\n });\n\n return result.doc;\n}\n\n/**\n * Apply a transform style to a MarkdownDocument.\n * Pipeline: MarkdownDocument → Doc → applyTransform → docToMarkdown → MarkdownDocument\n */\nexport async function applyTransformToMarkdown(\n markdownDoc: MarkdownDocument,\n transformStyle: string,\n themeId?: string,\n autoTemplates?: boolean,\n): Promise<MarkdownDocument> {\n const { markdownToDoc, docToMarkdown } = await import('@bendyline/squisq/doc');\n\n const doc = markdownToDoc(markdownDoc, { autoTemplates });\n const transformed = await applyTransformToDoc(doc, transformStyle, themeId);\n\n return docToMarkdown(transformed);\n}\n","/**\n * video command\n *\n * Renders a squisq document to MP4 video by delegating to the\n * programmatic renderDocToMp4 API.\n *\n * Usage:\n * squisq video <input> [-o output.mp4] [--fps 30] [--quality normal] [--orientation landscape]\n * [--theme <id>] [--transform <style>] [--cover-preroll <seconds>]\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 {\n applyTransformToDoc,\n assertValidThemeId,\n assertValidTransformStyle,\n} from '../util/applyTransformPipeline.js';\nimport { renderDocToMp4 } from '../api.js';\n\nimport type { VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\n\ntype CaptionOption = 'off' | 'standard' | 'social';\n\ninterface VideoCommandOptions {\n autoTemplates?: boolean;\n output?: string;\n fps?: string;\n quality?: VideoQuality;\n orientation?: VideoOrientation;\n captions?: CaptionOption;\n width?: string;\n height?: string;\n theme?: string;\n transform?: string;\n coverPreroll?: 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(\n '--no-auto-templates',\n 'Disable content-aware template auto-picking for unannotated headings',\n )\n .option('-t, --theme <id>', 'Squisq theme ID to apply (e.g., documentary, cinematic, bold)')\n .option(\n '--transform <style>',\n 'Transform style to apply before rendering (e.g., documentary, magazine, minimal)',\n )\n .option(\n '--cover-preroll <seconds>',\n 'Seconds of cover-slide pre-roll before the story starts (default: 2)',\n '2',\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 const coverPreRoll = parseFloat(opts.coverPreroll ?? '2');\n if (isNaN(coverPreRoll) || coverPreRoll < 0) {\n throw new Error('Cover pre-roll must be a number of seconds >= 0');\n }\n\n // Validate the transform ID up front. The theme id is validated after the\n // doc is read (below), so a custom theme inlined in the doc is admitted\n // rather than rejected as \"unknown\".\n if (opts.transform) {\n await assertValidTransformStyle(opts.transform);\n }\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 result = await readInput(resolvedInput);\n const { container } = result;\n\n // Validate the theme id now that we have the doc: accept built-ins AND any\n // custom themes declared by the document itself.\n if (opts.theme) {\n await assertValidThemeId(opts.theme, result);\n }\n\n // ── Step 2: Get or parse Doc ────────────────────────────────────\n // readInput already resolved a Doc (markdown-shaped inputs use the default\n // auto-templating). Only re-derive when the author explicitly opted out via\n // --no-auto-templates and a markdown source is available.\n let doc: Doc = result.doc;\n if (result.markdownDoc) {\n if (opts.autoTemplates === false) {\n const { markdownToDoc } = await import('@bendyline/squisq/doc');\n doc = markdownToDoc(result.markdownDoc, { autoTemplates: false });\n }\n } else {\n console.error('Using pre-built Doc JSON');\n }\n\n // Apply transform / theme before rendering, mirroring how convert threads\n // them to its exporters.\n if (opts.transform) {\n doc = await applyTransformToDoc(doc, opts.transform, opts.theme);\n console.error(` Applied transform: ${opts.transform}`);\n }\n if (opts.theme) {\n doc = { ...doc, themeId: opts.theme };\n }\n\n console.error(\n `Rendering: ${fps} fps, quality: ${quality}, orientation: ${orientation}, captions: ${captions}`,\n );\n\n // ── Step 3: Render via programmatic API ─────────────────────────\n const result2 = await renderDocToMp4(doc, container, {\n outputPath,\n fps,\n quality: quality as VideoQuality,\n orientation: orientation as VideoOrientation,\n width: opts.width ? parseInt(opts.width, 10) : undefined,\n height: opts.height ? parseInt(opts.height, 10) : undefined,\n captionStyle,\n coverPreRoll,\n onProgress: (phase, percent) => {\n writeProgress(phase, percent, 100);\n },\n });\n\n clearProgress();\n console.error(\n ` ✓ ${outputPath} (${result2.duration.toFixed(1)}s, ${result2.frameCount} frames)`,\n );\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 * validate command\n *\n * Structural validation for squisq markdown documents — fast feedback for\n * authors and agents without opening a browser. Reports unknown template\n * names (with did-you-mean), unparsed `{[…]}` annotations, malformed\n * heading attributes, unresolved `connectsTo` targets, duplicate block ids,\n * unparseable data fences, and image references missing from the bundle.\n *\n * Exit code: 0 when clean (or warnings only), 1 when errors are present\n * (or any finding with --strict), 2 when the input could not be read.\n *\n * Usage:\n * squisq validate <input> [--json] [--strict]\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport { dirname, extname, join, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport type { DocDiagnostic } from '@bendyline/squisq/schemas';\nimport { validateMarkdownDoc } from '@bendyline/squisq/doc';\nimport { readInput } from '../util/readInput.js';\n\ninterface ValidateOpts {\n json?: boolean;\n strict?: boolean;\n}\n\nexport function registerValidateCommand(program: Command): void {\n program\n .command('validate')\n .description('Validate a squisq markdown document and report structural problems')\n .argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')\n .option('--json', 'Output diagnostics as JSON (for tooling and agents)')\n .option('--strict', 'Exit non-zero on warnings as well as errors')\n .action(async (inputPath: string, opts: ValidateOpts) => {\n try {\n const exitCode = await runValidate(inputPath, opts);\n process.exitCode = exitCode;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n process.exitCode = 2;\n }\n });\n}\n\nasync function runValidate(inputPath: string, opts: ValidateOpts): Promise<number> {\n const resolvedInput = resolve(inputPath);\n const { container, markdownDoc, doc } = await readInput(resolvedInput);\n\n if (!markdownDoc) {\n // Doc-JSON input: nothing markdown-level to validate. Surface any\n // diagnostics already recorded on the doc.\n const diagnostics = doc.diagnostics ?? [];\n return report(diagnostics, opts, 'doc.json input — markdown-level checks skipped');\n }\n\n // Asset check source: container file list (dbk/zip/folder) or, for a bare\n // .md file, the filesystem relative to the document's folder.\n const entries = await container.listFiles();\n const containerPaths = new Set(entries.map((e) => e.path));\n const isBareMarkdown =\n statSync(resolvedInput).isFile() &&\n !['.zip', '.dbk'].includes(extname(resolvedInput).toLowerCase());\n const baseDir = dirname(resolvedInput);\n const hasAsset = (path: string): boolean => {\n if (containerPaths.has(path) || containerPaths.has(path.replace(/^\\.\\//, ''))) return true;\n if (isBareMarkdown) return existsSync(join(baseDir, path));\n return false;\n };\n\n const result = validateMarkdownDoc(markdownDoc, { assets: hasAsset });\n return report(result.diagnostics, opts);\n}\n\n/** Fixed-width, severity-aware line label. */\nfunction severityLabel(severity: DocDiagnostic['severity']): string {\n switch (severity) {\n case 'error':\n return 'ERROR ';\n case 'warning':\n return 'warning';\n case 'info':\n return 'ℹ info ';\n }\n}\n\nfunction report(diagnostics: DocDiagnostic[], opts: ValidateOpts, note?: string): number {\n const errorCount = diagnostics.filter((d) => d.severity === 'error').length;\n const warningCount = diagnostics.filter((d) => d.severity === 'warning').length;\n const infoCount = diagnostics.filter((d) => d.severity === 'info').length;\n\n if (opts.json) {\n // Machine-readable output goes to stdout so it can be piped/parsed;\n // everything human-facing follows the CLI convention of stderr.\n process.stdout.write(\n `${JSON.stringify({ errorCount, warningCount, infoCount, diagnostics }, null, 2)}\\n`,\n );\n } else {\n if (note) console.error(note);\n for (const d of diagnostics) {\n const location = d.line != null ? `line ${d.line}` : (d.blockId ?? '—');\n console.error(`${severityLabel(d.severity)} ${location} [${d.code}] ${d.message}`);\n }\n if (diagnostics.length === 0) {\n console.error('✓ No problems found');\n } else {\n console.error(`\\n${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info`);\n }\n }\n\n // Exit status is driven by errors only; --strict additionally fails on\n // warnings. Info diagnostics never affect the exit code.\n if (errorCount > 0) return 1;\n if (opts.strict && warningCount > 0) return 1;\n return 0;\n}\n","/**\n * doctor command\n *\n * Environment preflight for video rendering. Reports:\n * - ffmpeg: path, version, and which source found it (env / path /\n * ffmpeg-static), or an actionable install hint when missing\n * - Playwright Chromium: attempts a headless launch + immediate close\n * - Node version\n *\n * Exit code: 0 when everything `squisq video` needs is present, 1 otherwise.\n *\n * Usage:\n * squisq doctor\n */\n\nimport type { Command } from 'commander';\nimport { detectFfmpegDetailed, getFfmpegVersion } from '../util/detectFfmpeg.js';\n\nexport function registerDoctorCommand(program: Command): void {\n program\n .command('doctor')\n .description('Check that ffmpeg and Playwright Chromium are available for video rendering')\n .action(async () => {\n const allPresent = await runDoctor();\n process.exitCode = allPresent ? 0 : 1;\n });\n}\n\nasync function runDoctor(): Promise<boolean> {\n let allPresent = true;\n\n // ── Node ─────────────────────────────────────────────────────────\n console.error(`Node: ${process.version}`);\n\n // ── ffmpeg ───────────────────────────────────────────────────────\n try {\n const detection = await detectFfmpegDetailed();\n if (detection) {\n const version = await getFfmpegVersion(detection.path);\n console.error(\n `ffmpeg: ${detection.path} (source: ${detection.source}) — ${version ?? 'version unknown'}`,\n );\n } else {\n allPresent = false;\n console.error('ffmpeg: not found.');\n console.error(' Install it with:');\n console.error(' macOS: brew install ffmpeg');\n console.error(' Ubuntu: sudo apt install ffmpeg');\n console.error(' Windows: winget install ffmpeg');\n console.error(' Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG.');\n }\n } catch (err: unknown) {\n // detectFfmpegDetailed throws when SQUISQ_FFMPEG is set but broken\n allPresent = false;\n console.error(`ffmpeg: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n // ── Playwright Chromium ──────────────────────────────────────────\n try {\n const { chromium } = await import('playwright-core');\n const browser = await chromium.launch({ headless: true });\n await browser.close();\n console.error(`Chromium: available (${chromium.executablePath()})`);\n } catch (err: unknown) {\n allPresent = false;\n const detail = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n console.error(`Chromium: not available — ${detail}`);\n console.error(' Run: npx playwright install chromium');\n }\n\n console.error(allPresent ? '\\n✓ All checks passed' : '\\n✗ Some checks failed');\n return allPresent;\n}\n"],"mappings":"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","names":["extname","join","ConversionError","readFile","join","defaultRegistry","resolve","version","writeFile","readFile","mkdir","rm","join","tmpdir","randomBytes","execFile","resolve","MemoryContentContainer","join","readFile","defaultRegistry","extname","program","ConversionError","join","mkdir","dirname","basename","extname","resolve","program","resolve","basename","extname","dirname","mkdir","markdownToDoc","dirname","extname","join","resolve","program","resolve","extname","dirname","join","program","version","require"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/convert.ts","../src/util/applyTransformPipeline.ts","../src/commands/video.ts","../src/commands/validate.ts","../src/commands/doctor.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 video <input> [output.mp4|output.gif] [options]\n * squisq --help\n */\n\nimport { createRequire } from 'node:module';\nimport { Command } from 'commander';\nimport { registerConvertCommand } from './commands/convert.js';\nimport { registerVideoCommand } from './commands/video.js';\nimport { registerValidateCommand } from './commands/validate.js';\nimport { registerDoctorCommand } from './commands/doctor.js';\n\n// Read the real version from package.json at runtime (ESM-safe). From the built\n// `dist/index.js`, `../package.json` resolves to `packages/cli/package.json`.\nconst require = createRequire(import.meta.url);\nconst { version } = require('../package.json') as { version: string };\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[33mv${version}\\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(version);\n\nregisterConvertCommand(program);\nregisterVideoCommand(program);\nregisterValidateCommand(program);\nregisterDoctorCommand(program);\n\nprogram.parse();\n","/**\n * convert command\n *\n * Reads any supported input — a markdown file, a JSON Doc, a `.zip`/`.dbk`\n * container, a folder, or an importable binary (`.docx`/`.pptx`/`.pdf`/\n * `.xlsx`/`.csv`/`.html`) — and exports it to one or more target formats via\n * the shared format registry's `convert()`.\n *\n * Supports optional --theme and --transform flags to apply a squisq theme\n * and/or transform style before exporting.\n *\n * Usage:\n * squisq convert <input> [--output-dir <dir>] [--formats <list>] [--theme <id>] [--transform <style>]\n * squisq convert <input> -o <file> # single output; format inferred from extension\n * squisq convert <input> --formats <id> # single format to the default output dir\n */\n\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { dirname, basename, extname, join, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport { BUILTIN_FORMAT_IDS, ConversionError } from '@bendyline/squisq-formats';\nimport type { ConvertSource, FormatId } from '@bendyline/squisq-formats';\nimport { readInput } from '../util/readInput.js';\nimport { createCliRegistry } from '../registry.js';\nimport { convert } from '../api.js';\nimport { assertValidThemeId, assertValidTransformStyle } from '../util/applyTransformPipeline.js';\n\n/** Every format the registry can export (built-ins + CLI rendered media). */\nconst VALID_FORMATS: readonly FormatId[] = [...BUILTIN_FORMAT_IDS, 'mp4', 'gif'];\n\n/**\n * Default formats produced by a bare `convert <input>` (no -o / --formats).\n * Deliberately excludes md/xlsx/csv and — crucially — mp4/gif, so a\n * bare convert never spins up Playwright/FFmpeg.\n */\nconst DEFAULT_FORMATS: readonly FormatId[] = [\n 'docx',\n 'pptx',\n 'pdf',\n 'html',\n 'htmlzip',\n 'epub',\n 'dbk',\n];\n\nfunction isValidFormat(id: string): boolean {\n return VALID_FORMATS.includes(id);\n}\n\nfunction parseFormats(value: string): FormatId[] {\n const requested = value.split(',').map((s) => s.trim().toLowerCase());\n const valid: FormatId[] = [];\n for (const r of requested) {\n if (isValidFormat(r)) {\n valid.push(r);\n } else {\n console.warn(`Unknown format \"${r}\" — skipping. Valid: ${VALID_FORMATS.join(', ')}`);\n }\n }\n if (valid.length === 0) {\n throw new Error(`No valid formats specified. Valid: ${VALID_FORMATS.join(', ')}`);\n }\n return valid;\n}\n\n/** Infer a target format id from an explicit output file path's extension. */\nfunction formatFromOutputPath(outputPath: string): FormatId {\n const lower = outputPath.toLowerCase();\n if (lower.endsWith('.html.zip')) return 'htmlzip';\n const ext = extname(lower);\n const def = createCliRegistry().byExtension(ext);\n if (!def) {\n throw new Error(\n `Cannot infer a format from output extension \"${ext || '(none)'}\". ` +\n `Use --formats to specify one. Valid: ${VALID_FORMATS.join(', ')}`,\n );\n }\n return def.id;\n}\n\ninterface ConvertOpts {\n output?: string;\n outputDir?: string;\n formats?: string;\n theme?: string;\n transform?: string;\n autoTemplates?: boolean;\n /** Commander `--no-infer-theme` negation: defaults true, false when passed. */\n inferTheme?: boolean;\n /** Commander `--no-infer-layouts` negation: defaults true, false when passed. */\n inferLayouts?: boolean;\n /** Explicit rendered-media animation preference; undefined uses format defaults. */\n animations?: boolean;\n}\n\nexport function registerConvertCommand(program: Command): void {\n program\n .command('convert')\n .description(\n 'Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, animated GIF, and container formats',\n )\n .argument('<input>', 'Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder')\n .option('-o, --output <file>', 'Single output file (format inferred from its extension)')\n .option(\n '-d, --output-dir <dir>',\n 'Output directory for multi-format export (default: same as input)',\n )\n .option(\n '-f, --formats <list>',\n `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(', ')}`,\n )\n .option('-t, --theme <id>', 'Squisq theme ID to apply (e.g., documentary, cinematic, bold)')\n .option(\n '--transform <style>',\n 'Transform style to apply before export (e.g., documentary, magazine, minimal)',\n )\n .option(\n '--no-auto-templates',\n 'Disable content-aware template auto-picking for unannotated headings',\n )\n .option(\n '--no-infer-theme',\n 'Do not infer a Squisq theme from an office input file (PPTX theme colors/fonts)',\n )\n .option('--no-infer-layouts', 'Do not derive custom layout templates from PPTX slide layouts')\n .option('--animations', 'Enable slide animations/transitions in rendered media')\n .option('--no-animations', 'Disable slide animations/transitions in rendered media')\n .action(async (inputPath: string, opts: ConvertOpts) => {\n try {\n await runConvert(inputPath, opts);\n } catch (err: unknown) {\n if (err instanceof ConversionError) {\n console.error(`Error: ${err.message}`);\n if (err.hint) console.error(` ${err.hint}`);\n } else {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n }\n process.exitCode = 1;\n }\n });\n}\n\n/** Resolve the set of target formats from the mutually-exclusive flags. */\nfunction resolveFormats(opts: ConvertOpts): FormatId[] {\n const hasOutput = Boolean(opts.output);\n const hasFormatSelection = Boolean(opts.formats);\n\n if (hasOutput && hasFormatSelection) {\n throw new Error('--output is a single-file destination and cannot be combined with --formats.');\n }\n\n if (hasOutput) {\n return [formatFromOutputPath(opts.output!)];\n }\n if (opts.formats) {\n return parseFormats(opts.formats);\n }\n return [...DEFAULT_FORMATS];\n}\n\nasync function runConvert(inputPath: string, opts: ConvertOpts): Promise<void> {\n const resolvedInput = resolve(inputPath);\n\n const formats = resolveFormats(opts);\n\n // Base filename (used for suggested output names in multi-format mode).\n const inputBasename = basename(resolvedInput);\n const inputExt = extname(inputBasename);\n const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;\n\n // Validate the transform id up front (the style registry is static). The\n // theme id is validated after the doc is read, so a custom theme inlined in\n // the doc's frontmatter is admitted rather than rejected as \"unknown\".\n if (opts.transform) {\n await assertValidTransformStyle(opts.transform);\n }\n\n console.error(`Reading: ${resolvedInput}`);\n const result = await readInput(resolvedInput, {\n inferTheme: opts.inferTheme,\n inferLayouts: opts.inferLayouts,\n });\n\n if (opts.theme) {\n await assertValidThemeId(opts.theme, result);\n }\n\n // Build the convert() source once: prefer the markdown shape (so auto-\n // templating options apply), else the pre-built Doc.\n const source: ConvertSource = result.markdownDoc\n ? {\n kind: 'markdown',\n markdown: result.markdownDoc,\n container: result.container,\n baseName,\n }\n : { kind: 'doc', doc: result.doc, container: result.container, baseName };\n\n if (opts.transform) {\n console.error(` Applying transform: ${opts.transform}`);\n }\n\n for (const format of formats) {\n const formatOptions =\n (format === 'mp4' || format === 'gif') && opts.animations !== undefined\n ? { [format]: { animationsEnabled: opts.animations } }\n : undefined;\n const conversion = await convert(source, format, {\n themeId: opts.theme,\n transformStyle: opts.transform,\n autoTemplates: opts.autoTemplates,\n title: baseName,\n formatOptions,\n });\n\n for (const warning of conversion.warnings) {\n console.error(` ⚠ ${warning}`);\n }\n\n const outPath = opts.output\n ? resolve(opts.output)\n : join(\n opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput),\n conversion.suggestedFilename,\n );\n\n await mkdir(dirname(outPath), { recursive: true });\n await writeFile(outPath, Buffer.from(conversion.bytes));\n console.error(` ✓ ${outPath}`);\n }\n\n console.error('Done.');\n}\n","/**\n * applyTransformPipeline\n *\n * Shared `--theme` / `--transform` option handling for the convert and video\n * commands: option validation plus the transform application pipeline.\n *\n * Validation split (mirrors the original convert behavior):\n * - Transform styles can be validated up front — the style registry is static.\n * - Theme ids are validated only after the input has been read, so custom\n * themes inlined in the doc (frontmatter `squisq-custom-themes` or\n * `Doc.customThemes`) are admitted rather than rejected as \"unknown\".\n */\n\nimport type { MarkdownDocument } from '@bendyline/squisq/markdown';\nimport type { Doc } from '@bendyline/squisq/schemas';\n\n/**\n * Throw if `style` is not a registered transform style id.\n */\nexport async function assertValidTransformStyle(style: string): Promise<void> {\n const { getTransformStyleIds } = await import('@bendyline/squisq/transform');\n const styles = getTransformStyleIds();\n if (!styles.includes(style)) {\n throw new Error(`Unknown transform style \"${style}\". Available: ${styles.join(', ')}`);\n }\n}\n\n/**\n * Throw if `themeId` is neither a built-in theme nor a custom theme declared\n * by the document. Custom themes are read from the markdown frontmatter when\n * a MarkdownDocument is available, otherwise from `Doc.customThemes` (the\n * pre-built Doc JSON case).\n */\nexport async function assertValidThemeId(\n themeId: string,\n source: { markdownDoc?: MarkdownDocument | null; doc?: Doc | null },\n): Promise<void> {\n const { getAvailableThemes } = await import('@bendyline/squisq/schemas');\n const builtins = getAvailableThemes();\n\n let customIds: string[] = [];\n if (source.markdownDoc) {\n const { readCustomThemesFromFrontmatter } = await import('@bendyline/squisq/doc');\n customIds = (readCustomThemesFromFrontmatter(source.markdownDoc.frontmatter) ?? []).map(\n (t) => t.id,\n );\n } else if (source.doc?.customThemes) {\n customIds = source.doc.customThemes.map((t) => t.id);\n }\n\n if (!builtins.includes(themeId) && !customIds.includes(themeId)) {\n throw new Error(\n `Unknown theme \"${themeId}\". Available: ${[...builtins, ...customIds].join(', ')}`,\n );\n }\n}\n\n/**\n * Apply a transform style to a Doc.\n * Pipeline: Doc → extractDocImages → applyTransform → Doc\n */\nexport async function applyTransformToDoc(\n doc: Doc,\n transformStyle: string,\n themeId?: string,\n): Promise<Doc> {\n const { applyTransform, extractDocImages } = await import('@bendyline/squisq/transform');\n\n // Extract image metadata from the doc for transform interleaving\n const images = extractDocImages(doc.blocks);\n\n const result = applyTransform(doc, transformStyle, {\n themeId,\n images,\n });\n\n return result.doc;\n}\n\n/**\n * Apply a transform style to a MarkdownDocument.\n * Pipeline: MarkdownDocument → Doc → applyTransform → docToMarkdown → MarkdownDocument\n */\nexport async function applyTransformToMarkdown(\n markdownDoc: MarkdownDocument,\n transformStyle: string,\n themeId?: string,\n autoTemplates?: boolean,\n): Promise<MarkdownDocument> {\n const { markdownToDoc, docToMarkdown } = await import('@bendyline/squisq/doc');\n\n const doc = markdownToDoc(markdownDoc, { autoTemplates });\n const transformed = await applyTransformToDoc(doc, transformStyle, themeId);\n\n return docToMarkdown(transformed);\n}\n","/**\n * video command\n *\n * Renders a squisq document to MP4 or animated GIF by delegating to the\n * programmatic render APIs.\n *\n * Usage:\n * squisq video <input> [-o output.mp4|output.gif] [--format mp4|gif]\n * [--theme <id>] [--transform <style>] [--cover-preroll <seconds>]\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 {\n applyTransformToDoc,\n assertValidThemeId,\n assertValidTransformStyle,\n} from '../util/applyTransformPipeline.js';\nimport { renderDocToGif, renderDocToMp4 } from '../api.js';\n\nimport type { GifDither, VideoQuality, VideoOrientation } from '@bendyline/squisq-video';\n\ntype CaptionOption = 'off' | 'standard' | 'social';\ntype VideoOutputFormat = 'mp4' | 'gif';\n\ninterface VideoCommandOptions {\n autoTemplates?: boolean;\n output?: string;\n format?: string;\n fps?: string;\n quality?: VideoQuality;\n orientation?: VideoOrientation;\n captions?: CaptionOption;\n width?: string;\n height?: string;\n theme?: string;\n transform?: string;\n coverPreroll?: string;\n animations?: boolean;\n loop?: string;\n maxColors?: string;\n dither?: string;\n bayerScale?: 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;\nconst VALID_FORMATS = ['mp4', 'gif'] as const;\nconst VALID_GIF_DITHERS = ['bayer', 'sierra2_4a', 'none'] as const;\n\nexport function registerVideoCommand(program: Command): void {\n program\n .command('video')\n .description('Render a squisq document to MP4 video or animated GIF')\n .argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')\n .argument('[output]', 'Output .mp4 or .gif path (default: <input>.<format>)')\n .option('-o, --output <path>', 'Output .mp4 or .gif path (format inferred from extension)')\n .option('--format <format>', `Output format: ${VALID_FORMATS.join(', ')} (default: mp4)`)\n .option('--fps <number>', 'Frames per second (default: 30 for MP4, 10 for GIF)')\n .option(\n '--quality <level>',\n `MP4 encoding quality: ${VALID_QUALITIES.join(', ')} (default: 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(\n '--no-auto-templates',\n 'Disable content-aware template auto-picking for unannotated headings',\n )\n .option('-t, --theme <id>', 'Squisq theme ID to apply (e.g., documentary, cinematic, bold)')\n .option(\n '--transform <style>',\n 'Transform style to apply before rendering (e.g., documentary, magazine, minimal)',\n )\n .option(\n '--cover-preroll <seconds>',\n 'Seconds of cover-slide pre-roll before the story starts (default: 2)',\n '2',\n )\n .option('--width <pixels>', 'Override video width')\n .option('--height <pixels>', 'Override video height')\n .option('--animations', 'Enable slide animations/transitions (GIF default: disabled)')\n .option('--no-animations', 'Disable slide animations/transitions')\n .option('--loop <count>', 'GIF repeat count: 0 forever, -1 no loop (default: 0)')\n .option('--max-colors <count>', 'GIF palette size from 2 to 256 (default: 256)')\n .option(\n '--dither <algorithm>',\n `GIF dithering: ${VALID_GIF_DITHERS.join(', ')} (default: sierra2_4a)`,\n )\n .option('--bayer-scale <number>', 'GIF Bayer dither strength from 0 to 5 (default: 3)')\n .action(async (inputPath: string, outputArg: string | undefined, opts: VideoCommandOptions) => {\n try {\n // Use the positional output only when -o/--output was not supplied.\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 const requestedFormat = opts.format?.toLowerCase();\n if (\n requestedFormat !== undefined &&\n !VALID_FORMATS.includes(requestedFormat as VideoOutputFormat)\n ) {\n throw new Error(`Invalid format \"${requestedFormat}\". Valid: ${VALID_FORMATS.join(', ')}`);\n }\n const outputExtension = opts.output ? extname(opts.output).toLowerCase() : '';\n const extensionFormat: VideoOutputFormat | undefined =\n outputExtension === '.gif' ? 'gif' : outputExtension === '.mp4' ? 'mp4' : undefined;\n if (opts.output && !extensionFormat) {\n throw new Error('Output path must end in .mp4 or .gif');\n }\n if (requestedFormat && extensionFormat && requestedFormat !== extensionFormat) {\n throw new Error(\n `Output extension \"${outputExtension}\" conflicts with --format ${requestedFormat}.`,\n );\n }\n const outputFormat = (requestedFormat ?? extensionFormat ?? 'mp4') as VideoOutputFormat;\n\n // Resolve format-specific defaults before validating. GIF frame delays are\n // centisecond-based, so rates above 100 cannot be represented precisely.\n const maxFps = outputFormat === 'gif' ? 100 : 120;\n const fps = parseInt(opts.fps ?? (outputFormat === 'gif' ? '10' : '30'), 10);\n if (isNaN(fps) || fps < 1 || fps > maxFps) {\n throw new Error(`FPS must be a number between 1 and ${maxFps}`);\n }\n\n const quality = opts.quality ?? 'normal';\n if (outputFormat === 'gif' && opts.quality !== undefined) {\n throw new Error('--quality only applies to MP4 output');\n }\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 const coverPreRoll = parseFloat(opts.coverPreroll ?? '2');\n if (isNaN(coverPreRoll) || coverPreRoll < 0) {\n throw new Error('Cover pre-roll must be a number of seconds >= 0');\n }\n\n const animationsEnabled = opts.animations ?? outputFormat === 'mp4';\n const loop = opts.loop === undefined ? 0 : Number(opts.loop);\n const maxColors = opts.maxColors === undefined ? 256 : Number(opts.maxColors);\n const bayerScale = opts.bayerScale === undefined ? undefined : Number(opts.bayerScale);\n const dither = (opts.dither ?? 'sierra2_4a') as GifDither;\n if (outputFormat === 'mp4' && (opts.loop || opts.maxColors || opts.dither || opts.bayerScale)) {\n throw new Error('--loop, --max-colors, --dither, and --bayer-scale only apply to GIF output');\n }\n if (!Number.isSafeInteger(loop) || loop < -1 || loop > 65_535) {\n throw new Error('GIF loop must be an integer between -1 and 65535');\n }\n if (!Number.isSafeInteger(maxColors) || maxColors < 2 || maxColors > 256) {\n throw new Error('GIF max colors must be an integer between 2 and 256');\n }\n if (!VALID_GIF_DITHERS.includes(dither as (typeof VALID_GIF_DITHERS)[number])) {\n throw new Error(`Invalid GIF dither \"${dither}\". Valid: ${VALID_GIF_DITHERS.join(', ')}`);\n }\n if (\n bayerScale !== undefined &&\n (!Number.isSafeInteger(bayerScale) || bayerScale < 0 || bayerScale > 5)\n ) {\n throw new Error('GIF Bayer scale must be an integer between 0 and 5');\n }\n\n // Validate the transform ID up front. The theme id is validated after the\n // doc is read (below), so a custom theme inlined in the doc is admitted\n // rather than rejected as \"unknown\".\n if (opts.transform) {\n await assertValidTransformStyle(opts.transform);\n }\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}.${outputFormat}`);\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 result = await readInput(resolvedInput);\n const { container } = result;\n\n // Validate the theme id now that we have the doc: accept built-ins AND any\n // custom themes declared by the document itself.\n if (opts.theme) {\n await assertValidThemeId(opts.theme, result);\n }\n\n // ── Step 2: Get or parse Doc ────────────────────────────────────\n // readInput already resolved a Doc (markdown-shaped inputs use the default\n // auto-templating). Only re-derive when the author explicitly opted out via\n // --no-auto-templates and a markdown source is available.\n let doc: Doc = result.doc;\n if (result.markdownDoc) {\n if (opts.autoTemplates === false) {\n const { markdownToDoc, resolveAudioMapping } = await import('@bendyline/squisq/doc');\n // Re-deriving from markdown discards readInput's audio resolution\n // (narration timing + segment mapping) — re-run it.\n doc = await resolveAudioMapping(\n markdownToDoc(result.markdownDoc, { autoTemplates: false }),\n container,\n );\n }\n } else {\n console.error('Using pre-built Doc JSON');\n }\n\n // Apply transform / theme before rendering, mirroring how convert threads\n // them to its exporters.\n if (opts.transform) {\n doc = await applyTransformToDoc(doc, opts.transform, opts.theme);\n console.error(` Applied transform: ${opts.transform}`);\n }\n if (opts.theme) {\n doc = { ...doc, themeId: opts.theme };\n }\n\n const motionLabel = animationsEnabled ? 'animations on' : 'animations off';\n const qualityLabel = outputFormat === 'mp4' ? `, quality: ${quality}` : '';\n console.error(\n `Rendering ${outputFormat.toUpperCase()}: ${fps} fps${qualityLabel}, orientation: ${orientation}, captions: ${captions}, ${motionLabel}`,\n );\n\n // ── Step 3: Render via programmatic API ─────────────────────────\n const sharedRenderOptions = {\n outputPath,\n fps,\n orientation: orientation as VideoOrientation,\n width: opts.width ? parseInt(opts.width, 10) : undefined,\n height: opts.height ? parseInt(opts.height, 10) : undefined,\n captionStyle,\n coverPreRoll,\n animationsEnabled,\n onProgress: (phase: string, percent: number) => writeProgress(phase, percent, 100),\n };\n const result2 =\n outputFormat === 'gif'\n ? await renderDocToGif(doc, container, {\n ...sharedRenderOptions,\n loop,\n maxColors,\n dither,\n bayerScale,\n })\n : await renderDocToMp4(doc, container, {\n ...sharedRenderOptions,\n quality: quality as VideoQuality,\n });\n\n clearProgress();\n const warnings = 'warnings' in result2 && Array.isArray(result2.warnings) ? result2.warnings : [];\n for (const warning of warnings) {\n console.error(` ⚠ ${warning}`);\n }\n console.error(\n ` ✓ ${outputPath} (${result2.duration.toFixed(1)}s, ${result2.frameCount} frames)`,\n );\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 * validate command\n *\n * Structural validation for squisq markdown documents — fast feedback for\n * authors and agents without opening a browser. Reports unknown template\n * names (with did-you-mean), unparsed `{[…]}` annotations, malformed\n * heading attributes, unresolved `connectsTo` targets, duplicate block ids,\n * unparseable data fences, and image references missing from the bundle.\n *\n * Exit code: 0 when clean (or warnings only), 1 when errors are present\n * (or any finding with --strict), 2 when the input could not be read.\n *\n * Usage:\n * squisq validate <input> [--json] [--strict]\n */\n\nimport { existsSync, statSync } from 'node:fs';\nimport { dirname, extname, join, resolve } from 'node:path';\nimport type { Command } from 'commander';\nimport type { DocDiagnostic } from '@bendyline/squisq/schemas';\nimport { validateMarkdownDoc } from '@bendyline/squisq/doc';\nimport { readInput } from '../util/readInput.js';\n\ninterface ValidateOpts {\n json?: boolean;\n strict?: boolean;\n}\n\nexport function registerValidateCommand(program: Command): void {\n program\n .command('validate')\n .description('Validate a squisq markdown document and report structural problems')\n .argument('<input>', 'Path to .md file, .zip/.dbk container, or folder')\n .option('--json', 'Output diagnostics as JSON (for tooling and agents)')\n .option('--strict', 'Exit non-zero on warnings as well as errors')\n .action(async (inputPath: string, opts: ValidateOpts) => {\n try {\n const exitCode = await runValidate(inputPath, opts);\n process.exitCode = exitCode;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Error: ${message}`);\n process.exitCode = 2;\n }\n });\n}\n\nasync function runValidate(inputPath: string, opts: ValidateOpts): Promise<number> {\n const resolvedInput = resolve(inputPath);\n const { container, markdownDoc, doc } = await readInput(resolvedInput);\n\n if (!markdownDoc) {\n // Doc-JSON input: nothing markdown-level to validate. Surface any\n // diagnostics already recorded on the doc.\n const diagnostics = doc.diagnostics ?? [];\n return report(diagnostics, opts, 'doc.json input — markdown-level checks skipped');\n }\n\n // Asset check source: container file list (dbk/zip/folder) or, for a bare\n // .md file, the filesystem relative to the document's folder.\n const entries = await container.listFiles();\n const containerPaths = new Set(entries.map((e) => e.path));\n const isBareMarkdown =\n statSync(resolvedInput).isFile() &&\n !['.zip', '.dbk'].includes(extname(resolvedInput).toLowerCase());\n const baseDir = dirname(resolvedInput);\n const hasAsset = (path: string): boolean => {\n if (containerPaths.has(path) || containerPaths.has(path.replace(/^\\.\\//, ''))) return true;\n if (isBareMarkdown) return existsSync(join(baseDir, path));\n return false;\n };\n\n const result = validateMarkdownDoc(markdownDoc, { assets: hasAsset });\n return report(result.diagnostics, opts);\n}\n\n/** Fixed-width, severity-aware line label. */\nfunction severityLabel(severity: DocDiagnostic['severity']): string {\n switch (severity) {\n case 'error':\n return 'ERROR ';\n case 'warning':\n return 'warning';\n case 'info':\n return 'ℹ info ';\n }\n}\n\nfunction report(diagnostics: DocDiagnostic[], opts: ValidateOpts, note?: string): number {\n const errorCount = diagnostics.filter((d) => d.severity === 'error').length;\n const warningCount = diagnostics.filter((d) => d.severity === 'warning').length;\n const infoCount = diagnostics.filter((d) => d.severity === 'info').length;\n\n if (opts.json) {\n // Machine-readable output goes to stdout so it can be piped/parsed;\n // everything human-facing follows the CLI convention of stderr.\n process.stdout.write(\n `${JSON.stringify({ errorCount, warningCount, infoCount, diagnostics }, null, 2)}\\n`,\n );\n } else {\n if (note) console.error(note);\n for (const d of diagnostics) {\n const location = d.line != null ? `line ${d.line}` : (d.blockId ?? '—');\n console.error(`${severityLabel(d.severity)} ${location} [${d.code}] ${d.message}`);\n }\n if (diagnostics.length === 0) {\n console.error('✓ No problems found');\n } else {\n console.error(`\\n${errorCount} error(s), ${warningCount} warning(s), ${infoCount} info`);\n }\n }\n\n // Exit status is driven by errors only; --strict additionally fails on\n // warnings. Info diagnostics never affect the exit code.\n if (errorCount > 0) return 1;\n if (opts.strict && warningCount > 0) return 1;\n return 0;\n}\n","/**\n * doctor command\n *\n * Environment preflight for video rendering. Reports:\n * - ffmpeg: path, version, and which source found it (env / path /\n * ffmpeg-static), or an actionable install hint when missing\n * - Playwright Chromium: attempts a headless launch + immediate close\n * - Node version\n *\n * Exit code: 0 when everything `squisq video` needs is present, 1 otherwise.\n *\n * Usage:\n * squisq doctor\n */\n\nimport type { Command } from 'commander';\nimport { detectFfmpegDetailed, getFfmpegVersion } from '../util/detectFfmpeg.js';\n\nexport function registerDoctorCommand(program: Command): void {\n program\n .command('doctor')\n .description('Check that ffmpeg and Playwright Chromium are available for video rendering')\n .action(async () => {\n const allPresent = await runDoctor();\n process.exitCode = allPresent ? 0 : 1;\n });\n}\n\nasync function runDoctor(): Promise<boolean> {\n let allPresent = true;\n\n // ── Node ─────────────────────────────────────────────────────────\n console.error(`Node: ${process.version}`);\n\n // ── ffmpeg ───────────────────────────────────────────────────────\n try {\n const detection = await detectFfmpegDetailed();\n if (detection) {\n const version = await getFfmpegVersion(detection.path);\n console.error(\n `ffmpeg: ${detection.path} (source: ${detection.source}) — ${version ?? 'version unknown'}`,\n );\n } else {\n allPresent = false;\n console.error('ffmpeg: not found.');\n console.error(' Install it with:');\n console.error(' macOS: brew install ffmpeg');\n console.error(' Ubuntu: sudo apt install ffmpeg');\n console.error(' Windows: winget install ffmpeg');\n console.error(' Or: npm install ffmpeg-static, or set SQUISQ_FFMPEG.');\n }\n } catch (err: unknown) {\n // detectFfmpegDetailed throws when SQUISQ_FFMPEG is set but broken\n allPresent = false;\n console.error(`ffmpeg: ${err instanceof Error ? err.message : String(err)}`);\n }\n\n // ── Playwright Chromium ──────────────────────────────────────────\n try {\n const { chromium } = await import('playwright-core');\n const browser = await chromium.launch({ headless: true });\n await browser.close();\n console.error(`Chromium: available (${chromium.executablePath()})`);\n } catch (err: unknown) {\n allPresent = false;\n const detail = err instanceof Error ? err.message.split('\\n')[0] : String(err);\n console.error(`Chromium: not available — ${detail}`);\n console.error(' Run: npx playwright install chromium');\n }\n\n console.error(allPresent ? '\\n✓ All checks passed' : '\\n✗ Some checks failed');\n return allPresent;\n}\n"],"mappings":"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","names":["program","mkdir","dirname","basename","extname","resolve","VALID_FORMATS","program","resolve","extname","basename","dirname","mkdir","dirname","extname","join","resolve","program","resolve","extname","dirname","join","program","version","require"]}
@@ -0,0 +1,15 @@
1
+ import {
2
+ GIF_EXPORT_DEFAULTS,
3
+ framesToGifNative,
4
+ framesToGifNativeBytes,
5
+ framesToMp4Native,
6
+ framesToMp4NativeBytes
7
+ } from "./chunk-ESFEFKY4.js";
8
+ export {
9
+ GIF_EXPORT_DEFAULTS,
10
+ framesToGifNative,
11
+ framesToGifNativeBytes,
12
+ framesToMp4Native,
13
+ framesToMp4NativeBytes
14
+ };
15
+ //# sourceMappingURL=nativeEncoder-IQPN2RMC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ GIF_EXPORT_DEFAULTS,
4
+ framesToGifNative,
5
+ framesToGifNativeBytes,
6
+ framesToMp4Native,
7
+ framesToMp4NativeBytes
8
+ } from "./chunk-VM3ZFP3J.js";
9
+ export {
10
+ GIF_EXPORT_DEFAULTS,
11
+ framesToGifNative,
12
+ framesToGifNativeBytes,
13
+ framesToMp4Native,
14
+ framesToMp4NativeBytes
15
+ };
16
+ //# sourceMappingURL=nativeEncoder-WHUIQZLL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-cli",
3
- "version": "1.2.1",
4
- "description": "Squisq CLI — convert markdown documents to DOCX, PDF, HTML, and more",
3
+ "version": "2.0.0",
4
+ "description": "Squisq CLI — convert documents and render MP4 video or animated GIF",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
7
7
  "repository": {
@@ -17,6 +17,7 @@
17
17
  "pdf",
18
18
  "html",
19
19
  "converter",
20
+ "gif",
20
21
  "cli"
21
22
  ],
22
23
  "publishConfig": {
@@ -49,11 +50,10 @@
49
50
  "test": "mocha"
50
51
  },
51
52
  "dependencies": {
52
- "@bendyline/squisq": "1.5.1",
53
- "@bendyline/squisq-formats": "1.4.1",
54
- "@bendyline/squisq-react": "1.4.1",
55
- "@bendyline/squisq-video": "1.2.1",
56
- "@xmldom/xmldom": "0.9.10",
53
+ "@bendyline/squisq": "2.0.0",
54
+ "@bendyline/squisq-formats": "2.0.0",
55
+ "@bendyline/squisq-react": "2.0.0",
56
+ "@bendyline/squisq-video": "2.0.0",
57
57
  "commander": "12.1.0",
58
58
  "playwright-core": "1.58.2",
59
59
  "vite": "8.0.14"
@@ -1,86 +0,0 @@
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 {
8
- resolveDimensions,
9
- ffmpegVideoQualityArgs,
10
- audioBitrateArg
11
- } from "@bendyline/squisq-video";
12
- async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
13
- const fps = options.fps ?? 30;
14
- const quality = options.quality ?? "normal";
15
- const { width, height } = resolveDimensions(options);
16
- const onProgress = options.onProgress;
17
- if (frames.length === 0) {
18
- throw new Error("No frames provided for encoding");
19
- }
20
- const tmpId = randomBytes(8).toString("hex");
21
- const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
22
- await mkdir(workDir, { recursive: true });
23
- try {
24
- onProgress?.(0, "writing frames");
25
- const padLen = String(frames.length).length;
26
- for (let i = 0; i < frames.length; i++) {
27
- const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
28
- await writeFile(join(workDir, name), frames[i]);
29
- if (onProgress && i % 10 === 0) {
30
- onProgress(Math.round(i / frames.length * 30), "writing frames");
31
- }
32
- }
33
- let audioPath = null;
34
- if (audio) {
35
- audioPath = join(workDir, "audio-input");
36
- await writeFile(audioPath, audio);
37
- }
38
- onProgress?.(30, "encoding");
39
- const padPattern = join(workDir, `frame-%0${padLen}d.png`);
40
- const args = ["-y", "-framerate", String(fps), "-i", padPattern];
41
- if (audioPath) {
42
- args.push("-i", audioPath);
43
- }
44
- args.push(
45
- "-c:v",
46
- "libx264",
47
- ...ffmpegVideoQualityArgs(quality),
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", audioBitrateArg(quality), "-shortest");
55
- }
56
- args.push(outputPath);
57
- await new Promise((resolve, reject) => {
58
- execFile(ffmpegPath, args, { timeout: 6e5 }, (err) => {
59
- if (err) {
60
- reject(new Error(`ffmpeg failed: ${err.message}`));
61
- } else {
62
- resolve();
63
- }
64
- });
65
- });
66
- onProgress?.(100, "done");
67
- } finally {
68
- await rm(workDir, { recursive: true, force: true });
69
- }
70
- }
71
- async function framesToMp4NativeBytes(ffmpegPath, frames, audio, options = {}) {
72
- const tmpId = randomBytes(8).toString("hex");
73
- const tmpOutput = join(tmpdir(), `squisq-video-out-${tmpId}.mp4`);
74
- try {
75
- await framesToMp4Native(ffmpegPath, frames, audio, tmpOutput, options);
76
- const data = await readFile(tmpOutput);
77
- return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
78
- } finally {
79
- await rm(tmpOutput, { force: true });
80
- }
81
- }
82
- export {
83
- framesToMp4Native,
84
- framesToMp4NativeBytes
85
- };
86
- //# sourceMappingURL=nativeEncoder-2HGWDEZZ.js.map
@@ -1 +0,0 @@
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 {\n resolveDimensions,\n ffmpegVideoQualityArgs,\n audioBitrateArg,\n} 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 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 ...ffmpegVideoQualityArgs(quality),\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', audioBitrateArg(quality), '-shortest');\n }\n\n args.push(outputPath);\n\n // Run ffmpeg. execFile buffers stdout/stderr internally for the callback,\n // so the child's pipes are already drained — no manual stderr listener needed.\n await new Promise<void>((resolve, reject) => {\n 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\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":";AAWA,SAAS,gBAAgB;AACzB,SAAS,WAAW,UAAU,OAAO,UAAU;AAC/C,SAAS,YAAY;AACrB,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAG5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,eAAsB,kBACpB,YACA,QACA,OACA,YACA,UAA8B,CAAC,GAChB;AACf,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,EAAE,OAAO,OAAO,IAAI,kBAAkB,OAAO;AACnD,QAAM,aAAa,QAAQ;AAE3B,MAAI,OAAO,WAAW,GAAG;AACvB,UAAM,IAAI,MAAM,iCAAiC;AAAA,EACnD;AAGA,QAAM,QAAQ,YAAY,CAAC,EAAE,SAAS,KAAK;AAC3C,QAAM,UAAU,KAAK,OAAO,GAAG,gBAAgB,KAAK,EAAE;AACtD,QAAM,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC;AAExC,MAAI;AACF,iBAAa,GAAG,gBAAgB;AAGhC,UAAM,SAAS,OAAO,OAAO,MAAM,EAAE;AACrC,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,YAAM,OAAO,SAAS,OAAO,IAAI,CAAC,EAAE,SAAS,QAAQ,GAAG,CAAC;AACzD,YAAM,UAAU,KAAK,SAAS,IAAI,GAAG,OAAO,CAAC,CAAC;AAE9C,UAAI,cAAc,IAAI,OAAO,GAAG;AAC9B,mBAAW,KAAK,MAAO,IAAI,OAAO,SAAU,EAAE,GAAG,gBAAgB;AAAA,MACnE;AAAA,IACF;AAGA,QAAI,YAA2B;AAC/B,QAAI,OAAO;AACT,kBAAY,KAAK,SAAS,aAAa;AACvC,YAAM,UAAU,WAAW,KAAK;AAAA,IAClC;AAEA,iBAAa,IAAI,UAAU;AAG3B,UAAM,aAAa,KAAK,SAAS,WAAW,MAAM,OAAO;AACzD,UAAM,OAAO,CAAC,MAAM,cAAc,OAAO,GAAG,GAAG,MAAM,UAAU;AAE/D,QAAI,WAAW;AACb,WAAK,KAAK,MAAM,SAAS;AAAA,IAC3B;AAEA,SAAK;AAAA,MACH;AAAA,MACA;AAAA,MACA,GAAG,uBAAuB,OAAO;AAAA,MACjC;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,KAAK,IAAI,MAAM,6CAA6C,KAAK,IAAI,MAAM;AAAA,IACtF;AAEA,QAAI,WAAW;AACb,WAAK,KAAK,QAAQ,OAAO,QAAQ,gBAAgB,OAAO,GAAG,WAAW;AAAA,IACxE;AAEA,SAAK,KAAK,UAAU;AAIpB,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,eAAS,YAAY,MAAM,EAAE,SAAS,IAAQ,GAAG,CAAC,QAAQ;AACxD,YAAI,KAAK;AACP,iBAAO,IAAI,MAAM,kBAAkB,IAAI,OAAO,EAAE,CAAC;AAAA,QACnD,OAAO;AACL,kBAAQ;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,iBAAa,KAAK,MAAM;AAAA,EAC1B,UAAE;AAEA,UAAM,GAAG,SAAS,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAAA,EACpD;AACF;AAMA,eAAsB,uBACpB,YACA,QACA,OACA,UAA8B,CAAC,GACV;AACrB,QAAM,QAAQ,YAAY,CAAC,EAAE,SAAS,KAAK;AAC3C,QAAM,YAAY,KAAK,OAAO,GAAG,oBAAoB,KAAK,MAAM;AAEhE,MAAI;AACF,UAAM,kBAAkB,YAAY,QAAQ,OAAO,WAAW,OAAO;AACrE,UAAM,OAAO,MAAM,SAAS,SAAS;AACrC,WAAO,IAAI,WAAW,KAAK,QAAQ,KAAK,YAAY,KAAK,UAAU;AAAA,EACrE,UAAE;AACA,UAAM,GAAG,WAAW,EAAE,OAAO,KAAK,CAAC;AAAA,EACrC;AACF;","names":[]}
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/util/nativeEncoder.ts
4
- import { execFile } from "child_process";
5
- import { writeFile, readFile, mkdir, rm } from "fs/promises";
6
- import { join } from "path";
7
- import { tmpdir } from "os";
8
- import { randomBytes } from "crypto";
9
- import {
10
- resolveDimensions,
11
- ffmpegVideoQualityArgs,
12
- audioBitrateArg
13
- } from "@bendyline/squisq-video";
14
- async function framesToMp4Native(ffmpegPath, frames, audio, outputPath, options = {}) {
15
- const fps = options.fps ?? 30;
16
- const quality = options.quality ?? "normal";
17
- const { width, height } = resolveDimensions(options);
18
- const onProgress = options.onProgress;
19
- if (frames.length === 0) {
20
- throw new Error("No frames provided for encoding");
21
- }
22
- const tmpId = randomBytes(8).toString("hex");
23
- const workDir = join(tmpdir(), `squisq-video-${tmpId}`);
24
- await mkdir(workDir, { recursive: true });
25
- try {
26
- onProgress?.(0, "writing frames");
27
- const padLen = String(frames.length).length;
28
- for (let i = 0; i < frames.length; i++) {
29
- const name = `frame-${String(i + 1).padStart(padLen, "0")}.png`;
30
- await writeFile(join(workDir, name), frames[i]);
31
- if (onProgress && i % 10 === 0) {
32
- onProgress(Math.round(i / frames.length * 30), "writing frames");
33
- }
34
- }
35
- let audioPath = null;
36
- if (audio) {
37
- audioPath = join(workDir, "audio-input");
38
- await writeFile(audioPath, audio);
39
- }
40
- onProgress?.(30, "encoding");
41
- const padPattern = join(workDir, `frame-%0${padLen}d.png`);
42
- const args = ["-y", "-framerate", String(fps), "-i", padPattern];
43
- if (audioPath) {
44
- args.push("-i", audioPath);
45
- }
46
- args.push(
47
- "-c:v",
48
- "libx264",
49
- ...ffmpegVideoQualityArgs(quality),
50
- "-pix_fmt",
51
- "yuv420p",
52
- "-vf",
53
- `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
54
- );
55
- if (audioPath) {
56
- args.push("-c:a", "aac", "-b:a", audioBitrateArg(quality), "-shortest");
57
- }
58
- args.push(outputPath);
59
- await new Promise((resolve, reject) => {
60
- execFile(ffmpegPath, args, { timeout: 6e5 }, (err) => {
61
- if (err) {
62
- reject(new Error(`ffmpeg failed: ${err.message}`));
63
- } else {
64
- resolve();
65
- }
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-DLLAT2RT.js.map