Initial Commit
This commit is contained in:
98
openspec/archive/2026-05-18-font-compress-cli/proposal.md
Normal file
98
openspec/archive/2026-05-18-font-compress-cli/proposal.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user