# Capability Spec Delta: font-compress (font-compress-cli) **Package**: `@suaveplan/font-compress` **Status**: PROPOSED > Every requirement below carries a `**Verified by:**` line at authoring time. No exceptions. (See `openspec/AGENTS.md` Section 13.) ## ADDED Requirements ### Requirement: suaveplan-font-compress bin entry The package SHALL register a bin entry named `suaveplan-font-compress` mapping to `./dist/bin/suaveplan-font-compress.js`. The compiled bin script SHALL start with `#!/usr/bin/env node` and SHALL be executable. Invoking it from a consumer's `node_modules/.bin/` SHALL run the CLI's `main` function with `process.argv.slice(2)`. #### Scenario: bin field declared in package.json - **WHEN** the package's `package.json` is parsed - **THEN** `bin["suaveplan-font-compress"] === "./dist/bin/suaveplan-font-compress.js"` #### Scenario: shebang preserved in compiled output - **WHEN** `dist/bin/suaveplan-font-compress.js` is read after `bunx turbo build --filter=@suaveplan/font-compress` - **THEN** the first line is exactly `#!/usr/bin/env node` **Verified by:** `src/bin/suaveplan-font-compress.test.ts::"package.json bin field maps suaveplan-font-compress to dist/bin entry"` AND `src/bin/suaveplan-font-compress.spec-conformance.test.ts::"compiled bin starts with node shebang"` --- ### Requirement: Argument parser with help and version The package SHALL accept the following flags: - positional `` - `--input ` / `-i ` - `--output ` / `-o ` - `--format=btf|json` - `--force` / `-f` - `--recursive` / `-r` - `--no-recursive` - `--preserve-structure` - `--no-preserve-structure` - `--help` / `-h` - `--version` / `-v` `--help` SHALL print usage to stdout and exit `0`. `--version` SHALL print the package version (read from `package.json`) and exit `0`. Unknown flags SHALL print an error to stderr and exit `2`. Conflicting flags (e.g. `--recursive` and `--no-recursive` together) SHALL print an error to stderr and exit `2`. #### Scenario: --help short-circuits without doing FS work - **WHEN** the bin is invoked with `["--help"]` - **THEN** stdout contains the string `"USAGE"` AND the exit code is `0` AND no FS operations are performed (verified by spy on `node:fs/promises`) #### Scenario: --version reads from package.json - **WHEN** the bin is invoked with `["--version"]` - **THEN** stdout contains the same version string as `JSON.parse(readFileSync('package.json')).version` AND the exit code is `0` #### Scenario: unknown flag exits 2 - **WHEN** the bin is invoked with `["--no-such-flag"]` - **THEN** stderr contains the string `"Unknown option"` AND the exit code is `2` #### Scenario: conflicting recursive flags exit 2 - **WHEN** the bin is invoked with `["./fonts", "--recursive", "--no-recursive"]` - **THEN** the exit code is `2` AND stderr describes the conflict **Verified by:** `src/bin/suaveplan-font-compress.test.ts::"--help short-circuits with exit 0"` AND `src/bin/suaveplan-font-compress.test.ts::"--version prints package version with exit 0"` AND `src/bin/suaveplan-font-compress.test.ts::"unknown flag exits 2"` AND `src/bin/suaveplan-font-compress.test.ts::"conflicting recursive flags exit 2"` --- ### Requirement: Input dispatcher selects convert + pack vs pack-only The package SHALL inspect the input path's extension. For `.ttf`, `.otf`, `.woff`, `.woff2` (and directories containing them when `--recursive`) the bin SHALL call `@suaveplan/font-convert::FontConverter` (single file) or `BatchConverter` (directory) to produce a `TypefaceFont`, then call `BinaryFontPacker.pack(typefaceFont)` to emit a `Uint8Array`, then write the `.btf` file. For `.json` inputs (a previously-converted typeface JSON) the bin SHALL skip conversion and pack directly. #### Scenario: TTF input runs convert then pack - **WHEN** the bin is invoked with a single `.ttf` file path - **THEN** `FontConverter.convert` is called once AND `BinaryFontPacker.pack` is called once with the converter's output AND a `.btf` file is written next to the input (or under `--output` if specified) #### Scenario: JSON input runs pack only - **WHEN** the bin is invoked with a single `.json` file path containing a valid `TypefaceFont` - **THEN** `FontConverter.convert` is not called AND `BinaryFontPacker.pack` is called once with the parsed JSON AND a `.btf` file is written #### Scenario: directory input uses BatchConverter - **WHEN** the bin is invoked with a directory path containing two `.ttf` files at `--recursive` - **THEN** `BatchConverter` is called once with the directory AND `BinaryFontPacker.pack` is called twice AND two `.btf` files are written under `--output` **Verified by:** `src/bin/suaveplan-font-compress.test.ts::"TTF input runs convert then pack"` AND `src/bin/suaveplan-font-compress.test.ts::"JSON input runs pack only"` AND `src/bin/integration/cli-roundtrip.test.ts::"directory of TTFs produces matching BTF outputs"` --- ### Requirement: --format=json emits typeface JSON instead of BTF The package SHALL accept `--format=json` to emit the converter's `TypefaceFont` as JSON instead of running it through `BinaryFontPacker`. This mode SHALL write `.json` files. JSON-input + `--format=json` SHALL be a no-op pass-through (read JSON, validate, write to output path). #### Scenario: --format=json writes typeface JSON - **WHEN** the bin is invoked with a `.ttf` input and `--format=json` - **THEN** a `.json` file is written AND `BinaryFontPacker.pack` is not called AND the JSON parses as a valid `TypefaceFont` #### Scenario: default format is btf - **WHEN** the bin is invoked without `--format` - **THEN** `--format=btf` is the effective default AND `.btf` files are written **Verified by:** `src/bin/suaveplan-font-compress.test.ts::"--format=json writes typeface JSON instead of BTF"` AND `src/bin/suaveplan-font-compress.test.ts::"default format is btf"` --- ### Requirement: Structured exit codes The package SHALL return distinct exit codes: - `0` — all inputs processed successfully - `1` — one or more inputs failed during conversion or packing (per-file errors are isolated; the batch continues) - `2` — invalid CLI arguments (unknown flag, missing required positional, conflicting flags) - `3` — the input path does not exist on disk #### Scenario: missing input path exits 3 - **WHEN** the bin is invoked with a positional input that does not exist - **THEN** the exit code is `3` AND stderr contains the string `"does not exist"` #### Scenario: per-file failure exits 1, not 2 - **WHEN** the bin is invoked over a directory where one TTF file is corrupt and three are valid - **THEN** the exit code is `1` AND three `.btf` files are written AND the corrupt file's error is captured to stderr **Verified by:** `src/bin/suaveplan-font-compress.test.ts::"missing input path exits 3"` AND `src/bin/integration/cli-roundtrip.test.ts::"corrupt TTF in directory yields exit 1 with siblings still written"` --- ### Requirement: FONT_COMPRESS.CLI_INVALID_ARGS error code The package SHALL register `FONT_COMPRESS.CLI_INVALID_ARGS` in `FONT_COMPRESS_CODES` (the stable error-code registry). Argument-parsing failures SHALL throw `FontCompressError` with this code (caught by `main`'s top-level handler, which maps it to exit `2`). Plain `Error` SHALL NOT be thrown from the CLI argument-parsing path. #### Scenario: code is registered - **WHEN** `FONT_COMPRESS_CODES` is enumerated - **THEN** the array includes `"FONT_COMPRESS.CLI_INVALID_ARGS"` #### Scenario: bad args throw the coded error - **WHEN** the bin is invoked with an unknown flag and the throw is intercepted (in-process driver, not subprocess) - **THEN** a `FontCompressError` is thrown with `code === "FONT_COMPRESS.CLI_INVALID_ARGS"` AND `context.flag === "--no-such-flag"` **Verified by:** `src/error-codes.test.ts::"FONT_COMPRESS_CODES includes CLI_INVALID_ARGS"` AND `src/bin/suaveplan-font-compress.spec-conformance.test.ts::"argument-parser throws FontCompressError with CLI_INVALID_ARGS"` --- ### Requirement: Progress reporting and summary The package SHALL print per-file progress lines while processing a batch (one line per converted file with the family name and compression ratio) and a summary block at the end (total files, succeeded, failed, total original bytes, total compressed bytes, compression ratio, output path). Progress lines SHALL go to stdout. Errors SHALL go to stderr. #### Scenario: per-file progress lines printed - **WHEN** the bin is invoked over a directory with three valid TTFs - **THEN** stdout contains three lines starting with the per-file marker (`✓` or equivalent) AND each line names the family #### Scenario: summary block printed at end - **WHEN** the same invocation completes - **THEN** stdout ends with a summary block containing `Total fonts: 3`, `Converted: 3`, `Errors: 0` AND a compression-ratio line **Verified by:** `src/bin/integration/cli-roundtrip.test.ts::"per-file progress lines and summary block printed"`