Files
suaveplan/openspec/archive/2026-05-18-font-compress-cli/proposal.md
2026-05-27 23:29:53 +01:00

9.5 KiB

Font compress CLI

Package: @suaveplan/font-compress Status: PROPOSED Type: spec-conformance + feature

Why

The 2026-05-02 audit of @suaveplan/font-compress against legacy packages/universal/font-compress/ found that genesis ships only the library surface (BinaryFontPacker, BinaryFontUnpacker). Legacy ships a real font-compress bin (a 369-line cli.ts, registered as bin: { "font-compress": "./dist/cli.js" } in packages/universal/font-compress/package.json) that takes a TTF/OTF/WOFF/WOFF2 path or directory, runs it through batchConvertFonts from the legacy in-package converter, packs the output as .btf files, and writes a font registry JSON. Every existing consumer of the legacy package (font preprocessing pipelines, build steps, R3F asset baking) calls the bin, not the library API. Without it the package is unusable as a build-time tool.

Genesis correctly split the converter step out into @suaveplan/font-convert (FontConverter, BatchConverter), so the new CLI composes the two: convert → pack → write. The library surface stays unchanged; this change adds a thin bin layer that wires the two packages together with a conventional CLI argument parser, progress streaming, and structured exit codes.

What changes

  • ADDED: bin entry in package.json — "suaveplan-font-compress": "./dist/bin/suaveplan-font-compress.js". Bin name uses the genesis suaveplan-* convention (consistent with suaveplan-install).
  • ADDED: src/bin/suaveplan-font-compress.ts — the CLI entry point. Built by Vite into dist/bin/suaveplan-font-compress.js with a #!/usr/bin/env node shebang preserved.
  • ADDED: src/bin/suaveplan-font-compress.md — sibling doc covering invocation patterns, every flag, exit codes, and example pipelines.
  • ADDED: argument parser supporting:
    • positional <input> — file (.ttf/.otf/.woff/.woff2/.json) or directory path
    • --input <path> / -i <path> — explicit input (alternative to positional)
    • --output <path> / -o <path> — output directory
    • --format=btf|json — output format; default btf
    • --force / -f — overwrite existing .btf / .json files
    • --recursive / -r — recurse directories (default true)
    • --no-recursive — disable recursion
    • --preserve-structure / --no-preserve-structure — directory-layout strategy
    • --help / -h — print usage
    • --version / -v — print version (read from package.json)
  • ADDED: input dispatcher — detects whether <input> is typeface JSON (treat as already-converted, pack only) or a font binary (convert first via @suaveplan/font-convert, then pack). Single-file and directory inputs both supported; directory inputs use BatchConverter from @suaveplan/font-convert.
  • ADDED: --format=json mode — emit converted typeface JSON instead of BTF binary (useful for debugging the pipeline).
  • ADDED: structured exit codes:
    • 0 — all inputs processed successfully
    • 1 — one or more inputs failed during conversion or packing
    • 2 — invalid CLI arguments (unknown flag, missing required positional, conflicting flags)
    • 3 — input path does not exist
  • ADDED: error code FONT_COMPRESS.CLI_INVALID_ARGS registered on the package's existing FONT_COMPRESS_CODES registry (same pattern as BarcodeError); thrown on malformed CLI input before any FS work.
  • ADDED: dev dependency on @suaveplan/cli (for argument parsing, ANSI styling, progress bars) and @suaveplan/readline (for interactive prompts on overwrite confirmation).
  • MODIFIED: package.json bin field added; dependencies gain @suaveplan/cli, @suaveplan/readline, @suaveplan/logger; dependencies retain @suaveplan/font-convert (already present).

Out of scope

  • Changing the library surface (BinaryFontPacker, BinaryFontUnpacker) — they stay byte-for-byte identical. The CLI is purely a composition layer above them.
  • Re-merging font-convert back into font-compress — the genesis split is intentional (CLAUDE.md explicitly notes "the conversion step itself was intentionally split to @suaveplan/font-convert"). The CLI imports the converter; it does not re-host it.
  • Auto-installing as a bin on bun add @suaveplan/font-compress — that's a registry / lifecycle concern, not a spec concern. The bin field in package.json is enough; npm/bun do the rest.
  • WASM-backed conversion or hardware-accelerated packing — this CLI uses the existing TypeScript pipeline. A WASM optimisation is its own change.
  • Watch mode (--watch re-running on file changes) — punted; legacy doesn't have it. If demanded, add it later under a separate change.

Source mapping

This change derives from ../suaveplan/packages/universal/font-compress/src/cli.ts.

Requirement Source lines Notes
bin field shape ("font-compress": "./dist/cli.js") package.json L8-L10 Renamed: legacy bin is font-compress; new bin is suaveplan-font-compress (genesis convention)
showHelp printed usage cli.ts L33-L88 Reworded for genesis flag set; layout preserved
showVersion reading from package.json cli.ts L93-L99 Verbatim semantics; switch to ESM import of package.json
Argument parser (-h, -v, -f, -r, --no-recursive, --preserve-structure, --no-preserve-structure, -o, --output) cli.ts L104-L180 Verbatim flag set + adds --format=btf|json,-i/--input
Input-validation flow (existence, defaults based on outputSpecified) cli.ts L185-L240 Verbatim semantics
Progress callback (onProgress consuming ConversionProgress) cli.ts L253-L320 Verbatim; routed through @suaveplan/cli progress bar instead of inline ANSI codes
Summary block (totals, compression ratio, output paths) cli.ts L322-L353 Verbatim formatting
Exit codes 0 / 1 (success / errors) cli.ts L356-L362, L367-L368 Verbatim

Requirements with no source line

Requirement Justification
Exit codes 2 (bad args) and 3 (missing input) Legacy collapses both into exit 1. Genesis-wide guardrail — distinct exit codes let CI scripts tell "user typed it wrong" from "the work failed". Industry-standard convention (curl, ffmpeg, etc.).
--format=json mode Legacy is BTF-only. Adding the JSON path makes the CLI debuggable — operators can inspect the typeface intermediate before the binary pack — and it's a one-line addition since @suaveplan/font-convert already produces TypefaceFont JSON.
FONT_COMPRESS.CLI_INVALID_ARGS error code CLAUDE.md §Workflow rule 2 — every thrown error routes through @suaveplan/error with a stable code. Legacy uses process.exit(1) after console.error, which loses the error code.
Bin name suaveplan-font-compress (not font-compress) Genesis convention — suaveplan-install (@suaveplan/installer) is the existing precedent. Avoids global-namespace collisions with arbitrary font-compress bins from other authors.
Composition with @suaveplan/font-convert for binary input Genesis split the converter into its own package; the legacy CLI's in-package converter no longer exists in genesis. Importing @suaveplan/font-convert is the only way to handle TTF/OTF/WOFF/WOFF2 input.

Verification approach

  • src/bin/suaveplan-font-compress.test.ts — covers argument parsing (every flag, conflicting flags, missing positional, unknown flag), exit-code mapping, --help / --version short-circuit, dispatch between JSON-input and binary-input paths.
  • src/bin/suaveplan-font-compress.spec-conformance.test.ts — pins FONT_COMPRESS.CLI_INVALID_ARGS emission, structured logging through @suaveplan/logger, OTel span font-compress.cli.run recorded against an in-memory tracer, exit code never being thrown as a number from a non-main site.
  • src/bin/integration/cli-roundtrip.test.ts — integration test using a fixture TTF: invokes the bin via Bun.spawn, verifies stdout contains the success banner, verifies a .btf file is written, verifies the .btf round-trips through BinaryFontUnpacker to a TypefaceFont deep-equal to the converter output.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/font-compress to enumerate. As of 2026-05-02 there are no in-repo dependents:

  • No dependents (verified by grep -rl "@suaveplan/font-compress" /home/ubuntu/web/genesis/packages/ --include="package.json" returning only packages/fonts/font-compress/package.json itself).

This change is purely additive (new bin, new error code, new module under src/bin/). The library exports are unchanged, so the absence of dependents is not a risk gate.

Migration impact

None for library callers — BinaryFontPacker and BinaryFontUnpacker are unchanged. New for CLI callers — once the package is installed (bun add -D @suaveplan/font-compress) the suaveplan-font-compress bin is on node_modules/.bin/. Legacy callers using the old font-compress bin name will need to update their build scripts; this is captured in the ## Migration block of this proposal:

Migration

  • Old behavior: npx font-compress ./fonts -o ./output
  • New behavior: bunx suaveplan-font-compress ./fonts -o ./output (or add "compress:fonts": "suaveplan-font-compress ./fonts" to package.json scripts)
  • Search command consumers should run: grep -rln "\\bfont-compress\\b" . in their build scripts and CI workflows
  • Recommended replacement: rename invocations to suaveplan-font-compress. The flag set is fully back-compatible (every legacy flag still works); only the bin name changed, plus the new optional --format=json is added.