@bendyline/squisq-cli 2.2.0 → 2.3.1

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.
@@ -11,7 +11,7 @@ import {
11
11
  readInput,
12
12
  renderDocToGif,
13
13
  renderDocToMp4
14
- } from "./chunk-GYHQHKO5.js";
14
+ } from "./chunk-TPODSM5L.js";
15
15
  import {
16
16
  GIF_EXPORT_DEFAULTS,
17
17
  framesToGifNative,
package/dist/api.js CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "./chunk-Z3BHD4AF.js";
9
9
 
10
10
  // src/api.ts
11
+ import { readFile as readFile3 } from "fs/promises";
11
12
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
12
13
  import { flattenBlocks } from "@bendyline/squisq/doc";
13
14
  import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
@@ -96,7 +97,7 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
96
97
  return mixTimelineClips(ffmpegPath, usable, signal);
97
98
  }
98
99
  async function mixTimelineClips(ffmpegPath, usable, signal) {
99
- const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
100
+ const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
100
101
  const { join: join3 } = await import("path");
101
102
  const { tmpdir: tmpdir2 } = await import("os");
102
103
  const { randomBytes: randomBytes2 } = await import("crypto");
@@ -144,7 +145,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
144
145
  signal
145
146
  });
146
147
  signal?.throwIfAborted();
147
- const data = await readFile3(outputPath);
148
+ const data = await readFile4(outputPath);
148
149
  signal?.throwIfAborted();
149
150
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
150
151
  } finally {
@@ -663,17 +664,25 @@ function throwIfAborted(signal) {
663
664
 
664
665
  // src/api.ts
665
666
  import { ConversionError } from "@bendyline/squisq-formats";
667
+ var playerBundlePromise;
668
+ function loadPlayerBundle() {
669
+ playerBundlePromise ??= readFile3(
670
+ new URL("../dist/squisq-player.global.js", import.meta.url),
671
+ "utf8"
672
+ );
673
+ return playerBundlePromise;
674
+ }
666
675
  async function convert(source, to, options = {}) {
667
676
  return formatsConvert(source, to, {
668
677
  registry: createCliRegistry(),
669
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
678
+ resolvePlayerScript: loadPlayerBundle,
670
679
  ...options
671
680
  });
672
681
  }
673
682
  async function prepareConversion(source, options = {}) {
674
683
  return formatsPrepareConversion(source, {
675
684
  registry: createCliRegistry(),
676
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
685
+ resolvePlayerScript: loadPlayerBundle,
677
686
  ...options
678
687
  });
679
688
  }
@@ -732,10 +741,10 @@ async function captureDocFrames(doc, container, options) {
732
741
  }
733
742
  onProgress?.("generating render HTML", 10);
734
743
  signal?.throwIfAborted();
735
- const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
744
+ const playerBundle = await loadPlayerBundle();
736
745
  signal?.throwIfAborted();
737
746
  const renderHtml = generateRenderHtml(doc, {
738
- playerScript: PLAYER_BUNDLE,
747
+ playerScript: playerBundle,
739
748
  images,
740
749
  audio: audio.size > 0 ? audio : void 0,
741
750
  width,
@@ -5,6 +5,7 @@ import {
5
5
  } from "./chunk-GGVASRPG.js";
6
6
 
7
7
  // src/api.ts
8
+ import { readFile as readFile3 } from "fs/promises";
8
9
  import { resolveMediaSchedule } from "@bendyline/squisq/schemas";
9
10
  import { flattenBlocks } from "@bendyline/squisq/doc";
10
11
  import { ffmpegGifOutputArgs, generateRenderHtml } from "@bendyline/squisq-video";
@@ -93,7 +94,7 @@ async function buildMixedAudioTrack(doc, container, ffmpegPath, coverPreRoll, si
93
94
  return mixTimelineClips(ffmpegPath, usable, signal);
94
95
  }
95
96
  async function mixTimelineClips(ffmpegPath, usable, signal) {
96
- const { writeFile, readFile: readFile3, mkdir, rm: rm2 } = await import("fs/promises");
97
+ const { writeFile, readFile: readFile4, mkdir, rm: rm2 } = await import("fs/promises");
97
98
  const { join: join3 } = await import("path");
98
99
  const { tmpdir: tmpdir2 } = await import("os");
99
100
  const { randomBytes: randomBytes2 } = await import("crypto");
@@ -141,7 +142,7 @@ async function mixTimelineClips(ffmpegPath, usable, signal) {
141
142
  signal
142
143
  });
143
144
  signal?.throwIfAborted();
144
- const data = await readFile3(outputPath);
145
+ const data = await readFile4(outputPath);
145
146
  signal?.throwIfAborted();
146
147
  return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
147
148
  } finally {
@@ -307,7 +308,7 @@ function mp4Format() {
307
308
  const animationsEnabled = typeof mp4Opts.animationsEnabled === "boolean" ? mp4Opts.animationsEnabled : MP4_DEFAULTS.animationsEnabled;
308
309
  const outputPath = join(tmpdir(), `squisq-mp4-${randomBytes(8).toString("hex")}.mp4`);
309
310
  try {
310
- const { renderDocToMp4: renderDocToMp42 } = await import("./api-5SX73ZKT.js");
311
+ const { renderDocToMp4: renderDocToMp42 } = await import("./api-L5AORQHA.js");
311
312
  await renderDocToMp42(input.doc, input.container, {
312
313
  outputPath,
313
314
  fps,
@@ -347,7 +348,7 @@ function gifFormat() {
347
348
  const defaultHeight = portrait ? GIF_DEFAULTS.width : GIF_DEFAULTS.height;
348
349
  const outputPath = join(tmpdir(), `squisq-gif-${randomBytes(8).toString("hex")}.gif`);
349
350
  try {
350
- const { renderDocToGif: renderDocToGif2 } = await import("./api-5SX73ZKT.js");
351
+ const { renderDocToGif: renderDocToGif2 } = await import("./api-L5AORQHA.js");
351
352
  const result = await renderDocToGif2(input.doc, input.container, {
352
353
  outputPath,
353
354
  fps: typeof gifOpts.fps === "number" ? gifOpts.fps : GIF_DEFAULTS.fps,
@@ -660,17 +661,25 @@ function throwIfAborted(signal) {
660
661
 
661
662
  // src/api.ts
662
663
  import { ConversionError } from "@bendyline/squisq-formats";
664
+ var playerBundlePromise;
665
+ function loadPlayerBundle() {
666
+ playerBundlePromise ??= readFile3(
667
+ new URL("../dist/squisq-player.global.js", import.meta.url),
668
+ "utf8"
669
+ );
670
+ return playerBundlePromise;
671
+ }
663
672
  async function convert(source, to, options = {}) {
664
673
  return formatsConvert(source, to, {
665
674
  registry: createCliRegistry(),
666
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
675
+ resolvePlayerScript: loadPlayerBundle,
667
676
  ...options
668
677
  });
669
678
  }
670
679
  async function prepareConversion(source, options = {}) {
671
680
  return formatsPrepareConversion(source, {
672
681
  registry: createCliRegistry(),
673
- resolvePlayerScript: () => import("@bendyline/squisq-react/standalone-source").then((m) => m.PLAYER_BUNDLE),
682
+ resolvePlayerScript: loadPlayerBundle,
674
683
  ...options
675
684
  });
676
685
  }
@@ -729,10 +738,10 @@ async function captureDocFrames(doc, container, options) {
729
738
  }
730
739
  onProgress?.("generating render HTML", 10);
731
740
  signal?.throwIfAborted();
732
- const { PLAYER_BUNDLE } = await import("@bendyline/squisq-react/standalone-source");
741
+ const playerBundle = await loadPlayerBundle();
733
742
  signal?.throwIfAborted();
734
743
  const renderHtml = generateRenderHtml(doc, {
735
- playerScript: PLAYER_BUNDLE,
744
+ playerScript: playerBundle,
736
745
  images,
737
746
  audio: audio.size > 0 ? audio : void 0,
738
747
  width,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  readInput,
8
8
  renderDocToGif,
9
9
  renderDocToMp4
10
- } from "./chunk-GYHQHKO5.js";
10
+ } from "./chunk-TPODSM5L.js";
11
11
  import "./chunk-GGVASRPG.js";
12
12
 
13
13
  // src/index.ts
@@ -17,6 +17,7 @@ import { Command } from "commander";
17
17
  // src/commands/convert.ts
18
18
  import { mkdir } from "fs/promises";
19
19
  import { dirname, basename, extname, join, resolve } from "path";
20
+ import { Option } from "commander";
20
21
  import { BUILTIN_FORMAT_IDS, ConversionError } from "@bendyline/squisq-formats";
21
22
 
22
23
  // src/util/applyTransformPipeline.ts
@@ -163,6 +164,12 @@ function parseFormats(value) {
163
164
  `Unknown format${unknown.length > 1 ? "s" : ""}: ${hints}. Valid: ${VALID_FORMATS.join(", ")}`
164
165
  );
165
166
  }
167
+ const duplicates = [...new Set(requested.filter((id, index) => requested.indexOf(id) !== index))];
168
+ if (duplicates.length > 0) {
169
+ throw new Error(
170
+ `Duplicate format${duplicates.length > 1 ? "s" : ""}: ${duplicates.map((id) => `"${id}"`).join(", ")}. Each format may only be requested once.`
171
+ );
172
+ }
166
173
  return requested;
167
174
  }
168
175
  function formatFromOutputPath(outputPath) {
@@ -180,12 +187,21 @@ function formatFromOutputPath(outputPath) {
180
187
  function registerConvertCommand(program2) {
181
188
  program2.command("convert").description(
182
189
  "Convert a document to DOCX, PPTX, PDF, HTML, EPUB, MP4, animated GIF, and container formats"
183
- ).argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").option("-o, --output <file>", "Single output file (format inferred from its extension)").option(
184
- "-d, --output-dir <dir>",
185
- "Output directory for multi-format export (default: same as input)"
186
- ).option(
187
- "-f, --formats <list>",
188
- `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(", ")}`
190
+ ).argument("<input>", "Path to .md/.docx/.pptx/.pdf/.xlsx/.csv/.html file, .zip/.dbk, or folder").addOption(
191
+ new Option(
192
+ "-o, --output <file>",
193
+ "Single output file (format inferred from its extension)"
194
+ ).conflicts(["outputDir", "formats"])
195
+ ).addOption(
196
+ new Option(
197
+ "-d, --output-dir <dir>",
198
+ "Output directory for multi-format export (default: same as input)"
199
+ ).conflicts("output")
200
+ ).addOption(
201
+ new Option(
202
+ "-f, --formats <list>",
203
+ `Comma-separated formats to produce (default: a standard set). Valid: ${VALID_FORMATS.join(", ")}`
204
+ ).conflicts("output")
189
205
  ).option("--overwrite", "Replace existing output files (default: refuse and exit non-zero)").option("-t, --theme <id>", "Squisq theme ID to apply (e.g., documentary, cinematic, bold)").option(
190
206
  "--transform <style>",
191
207
  "Transform style to apply before export (e.g., documentary, magazine, minimal)"
@@ -296,7 +312,7 @@ var VALID_CAPTIONS = ["off", "standard", "social"];
296
312
  var VALID_FORMATS2 = ["mp4", "gif"];
297
313
  var VALID_GIF_DITHERS = ["bayer", "sierra2_4a", "none"];
298
314
  function registerVideoCommand(program2) {
299
- program2.command("video").description("Render a squisq document to MP4 video or animated GIF").argument("<input>", "Path to .md file, .zip/.dbk container, or folder").argument("[output]", "Output .mp4 or .gif path (default: <input>.<format>)").option("-o, --output <path>", "Output .mp4 or .gif path (format inferred from extension)").option("--format <format>", `Output format: ${VALID_FORMATS2.join(", ")} (default: mp4)`).option("--fps <number>", "Frames per second (default: 30 for MP4, 10 for GIF)").option(
315
+ program2.command("video").description("Render a squisq document to MP4 video or animated GIF").argument("<input>", "Path to .md/.json file, .zip/.dbk container, or folder").argument("[output]", "Output .mp4 or .gif path (default: <input>.<format>)").option("-o, --output <path>", "Output .mp4 or .gif path (format inferred from extension)").option("--format <format>", `Output format: ${VALID_FORMATS2.join(", ")} (default: mp4)`).option("--fps <number>", "Frames per second (default: 30 for MP4, 10 for GIF)").option(
300
316
  "--quality <level>",
301
317
  `MP4 encoding quality: ${VALID_QUALITIES.join(", ")} (default: normal)`
302
318
  ).option(
@@ -322,9 +338,10 @@ function registerVideoCommand(program2) {
322
338
  `GIF dithering: ${VALID_GIF_DITHERS.join(", ")} (default: sierra2_4a)`
323
339
  ).option("--bayer-scale <number>", "GIF Bayer dither strength from 0 to 5 (default: 3)").action(async (inputPath, outputArg, opts) => {
324
340
  try {
325
- if (outputArg && !opts.output) {
326
- opts.output = outputArg;
341
+ if (outputArg && opts.output) {
342
+ throw new Error("The positional output and --output cannot be used together.");
327
343
  }
344
+ if (outputArg) opts.output = outputArg;
328
345
  await runVideo(inputPath, opts);
329
346
  } catch (err) {
330
347
  const message = err instanceof Error ? err.message : String(err);
@@ -351,9 +368,9 @@ async function runVideo(inputPath, opts) {
351
368
  }
352
369
  const outputFormat = requestedFormat ?? extensionFormat ?? "mp4";
353
370
  const maxFps = outputFormat === "gif" ? 100 : 120;
354
- const fps = parseInt(opts.fps ?? (outputFormat === "gif" ? "10" : "30"), 10);
355
- if (isNaN(fps) || fps < 1 || fps > maxFps) {
356
- throw new Error(`FPS must be a number between 1 and ${maxFps}`);
371
+ const fps = Number(opts.fps ?? (outputFormat === "gif" ? "10" : "30"));
372
+ if (!Number.isSafeInteger(fps) || fps < 1 || fps > maxFps) {
373
+ throw new Error(`FPS must be an integer between 1 and ${maxFps}`);
357
374
  }
358
375
  const quality = opts.quality ?? "normal";
359
376
  if (outputFormat === "gif" && opts.quality !== void 0) {
@@ -368,16 +385,16 @@ async function runVideo(inputPath, opts) {
368
385
  `Invalid orientation "${orientation}". Valid: ${VALID_ORIENTATIONS.join(", ")}`
369
386
  );
370
387
  }
371
- const width = opts.width === void 0 ? void 0 : parseInt(opts.width, 10);
372
- const height = opts.height === void 0 ? void 0 : parseInt(opts.height, 10);
388
+ const width = opts.width === void 0 ? void 0 : Number(opts.width);
389
+ const height = opts.height === void 0 ? void 0 : Number(opts.height);
373
390
  validateVideoExportOptions({ width, height, orientation });
374
391
  const captions = opts.captions ?? "off";
375
392
  if (!VALID_CAPTIONS.includes(captions)) {
376
393
  throw new Error(`Invalid captions "${captions}". Valid: ${VALID_CAPTIONS.join(", ")}`);
377
394
  }
378
395
  const captionStyle = captions === "off" ? void 0 : captions;
379
- const coverPreRoll = parseFloat(opts.coverPreroll ?? "2");
380
- if (isNaN(coverPreRoll) || coverPreRoll < 0) {
396
+ const coverPreRoll = Number(opts.coverPreroll ?? "2");
397
+ if (!Number.isFinite(coverPreRoll) || coverPreRoll < 0) {
381
398
  throw new Error("Cover pre-roll must be a number of seconds >= 0");
382
399
  }
383
400
  const animationsEnabled = opts.animations ?? outputFormat === "mp4";
@@ -615,9 +632,11 @@ async function runDoctor(runtime = defaultDoctorRuntime) {
615
632
  // src/index.ts
616
633
  var require2 = createRequire(import.meta.url);
617
634
  var { version } = require2("../package.json");
618
- console.error(
619
- `\x1B[36m{[\x1B[0m \x1B[1msquiggly square\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[1msquisq\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[33mv${version}\x1B[0m \x1B[36m]}\x1B[0m`
620
- );
635
+ if (process.stderr.isTTY) {
636
+ console.error(
637
+ `\x1B[36m{[\x1B[0m \x1B[1msquiggly square\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[1msquisq\x1B[0m \x1B[2m\u2014\x1B[0m \x1B[33mv${version}\x1B[0m \x1B[36m]}\x1B[0m`
638
+ );
639
+ }
621
640
  var program = new Command();
622
641
  program.name("squisq").description("Squisq CLI \u2014 convert and process markdown-based documents").version(version);
623
642
  registerConvertCommand(program);