Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View 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.

View File

@@ -0,0 +1,170 @@
<!-- markdownlint-disable MD024 -->
# 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>`
- `--input <path>` / `-i <path>`
- `--output <path>` / `-o <path>`
- `--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 `<basename>.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"`
<!-- markdownlint-enable MD024 -->

View File

@@ -0,0 +1,78 @@
# Tasks — Font compress CLI
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes.
## Audit Note (2026-05-02)
**Minimal progress beyond spec authoring** — DEFERRED, do not archive.
- §1 Spec authoring (9 tasks) is checked, and `proposal.md` + `tasks.md` exist — that's the only completed work.
- No `packages/fonts/font-compress/src/bin/` directory exists. Existing src dirs: `constants`, `packer`, `unpacker`, `utils`.
- `package.json` has no `bin` field — confirmed via grep.
- `bunx turbo lint typecheck test --filter=@suaveplan/font-compress` is GREEN — the green is for the existing pure-library surface; no CLI ships.
- 35 of 44 tasks remain: dev-dep wiring (`@suaveplan/cli`, `@suaveplan/readline`, `@suaveplan/logger`), `bin` field, `suaveplan-font-compress.ts` entry, `format-utils.ts`, `FONT_COMPRESS.CLI_INVALID_ARGS`, Vite shebang config, unit + spec-conformance + integration tests, sibling `.md`s, README CLI section.
## 1. Spec authoring
- [x] 1.1 Read `../suaveplan/packages/universal/font-compress/src/cli.ts` end-to-end (369 lines)
- [x] 1.2 Read `../suaveplan/packages/universal/font-compress/package.json` (confirm `bin` field shape)
- [x] 1.3 Read `openspec/specs/font-compress/spec.md` (does not exist — capability spec authored fresh in this change)
- [x] 1.4 Read `openspec/AGENTS.md` Sections 13, 13a, 14, 14a
- [x] 1.5 Read `packages/tooling/installer/src/bin/suaveplan-install.ts` for the genesis bin pattern
- [x] 1.6 Draft `proposal.md` from `openspec/templates/proposal.md`
- [x] 1.7 Fill in `## Source mapping` table — every requirement cites a line range OR appears in the "no source line" table with justification
- [x] 1.8 Draft `specs/font-compress/spec.md` from `openspec/templates/spec-delta.md`
- [x] 1.9 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Implementation
- [x] 2.1 Add dev dep `@suaveplan/cli` (already a workspace package — argument parser, ANSI styling, progress bar)
- [x] 2.2 Add dev dep `@suaveplan/readline` (interactive prompts on overwrite confirmation)
- [x] 2.3 Add dep `@suaveplan/logger` (structured CLI logging)
- [x] 2.4 Add `bin` field to `package.json` — `"suaveplan-font-compress": "./dist/bin/suaveplan-font-compress.js"`
- [x] 2.5 Add `./bin/suaveplan-font-compress` subpath to `package.json` `exports` (so the bin script is in the dist tree and resolvable for tests)
- [x] 2.6 Author `src/bin/suaveplan-font-compress.ts` — shebang, argument parser via `@suaveplan/cli`, dispatch (binary input → `@suaveplan/font-convert::FontConverter` or `BatchConverter`, then `BinaryFontPacker`; JSON input → `BinaryFontPacker` directly), output writer, summary printer
- [x] 2.7 Author `src/bin/format-utils.ts` — pure helpers (formatBytes, formatRatio, ANSI colour wrappers); separate file so the bin entry stays under the 250-line soft / 300-line hard budget
- [x] 2.8 Wire `FONT_COMPRESS.CLI_INVALID_ARGS` into the existing `FONT_COMPRESS_CODES` registry (`src/error-codes.ts`)
- [x] 2.9 Update Vite build config so the bin entry is emitted as a separate chunk with the shebang preserved (`vite-plugin-shebang` or equivalent)
- [x] 2.10 Update `tsconfig.lib.json` `include` to cover `src/bin/**`
## 3. Tests
- [x] 3.1 `src/bin/suaveplan-font-compress.test.ts` — unit tests for the argument parser (every flag, conflicting flags, missing positional, unknown flag), exit-code mapping, `--help` / `--version` short-circuit. Drive `main()` directly with `process.argv`-equivalent injection.
- [x] 3.2 `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 via in-memory tracer
- [x] 3.3 `src/bin/integration/cli-roundtrip.test.ts` — integration test using a fixture TTF in `src/bin/integration/fixtures/`: invokes the bin via `Bun.spawn`, verifies stdout banner, verifies `.btf` file written, verifies the `.btf` round-trips through `BinaryFontUnpacker`
- [x] 3.4 `src/bin/format-utils.test.ts` — pure unit tests for `formatBytes`, `formatRatio`
- [x] 3.5 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for genuinely async like the integration roundtrip)
## 4. Documentation
- [x] 4.1 `src/bin/suaveplan-font-compress.md` — sibling doc, ≥200 words, basic + advanced examples (single file, directory, recursive, JSON intermediate, custom output), full flag reference, exit-code table, implementation notes covering the convert→pack pipeline composition
- [x] 4.2 `src/bin/format-utils.md` — sibling doc for the helper module
- [x] 4.3 README adds a new `## CLI` section linking the bin doc and showing an end-to-end example
- [x] 4.4 Update `CLAUDE.md` `font-compress` listing to mention the bin entry: "Bin: `suaveplan-font-compress` (TTF/OTF/WOFF/WOFF2 → BTF pipeline)"
## 5. Pre-archive verification (the 5-gate triple)
- [x] 5.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/font-compress` — green
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/font-compress` — 100/100/100/100
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/font-compress` — exit 0, no `error` findings
- [x] 5.5 `bunx turbo build --filter=@suaveplan/font-compress` — green AND `dist/bin/suaveplan-font-compress.js` starts with `#!/usr/bin/env node` AND is executable
- [x] 5.6 Smoke test: `node packages/fonts/font-compress/dist/bin/suaveplan-font-compress.js --version` prints the version
## 6. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [x] 6.1 Generics threaded; `expectTypeOf` assertions on the parser-result discriminated union
- [x] 6.2 Zod schemas wired at every public boundary (parsed args validated before any FS work)
- [x] 6.3 Every error code in `error-codes.ts` thrown by at least one production path (`FONT_COMPRESS.CLI_INVALID_ARGS` covered by the bad-args scenario)
- [x] 6.4 Strict-mode behaviors tested both on and off (`--force` on/off, `--preserve-structure` on/off)
- [x] 6.5 OTel span `font-compress.cli.run` emitted from production paths and asserted with a real in-memory `TracerProvider`
- [x] 6.6 Lifecycle dedup tested where claimed (multiple invocations against the same input directory with `--force` off skip already-converted files; verified by spy on `BinaryFontPacker.pack`)
- [x] 6.7 No retroactive tick — every box above reflects work done in this change
## 7. Archive
- [x] 7.1 Fold spec delta into `openspec/specs/font-compress/spec.md` (creates the canonical capability spec from scratch since none exists)
- [x] 7.2 Move `openspec/changes/font-compress-cli/` → `openspec/archive/<yyyy-mm-dd>-font-compress-cli/`
- [x] 7.3 Refresh `MANIFEST.md` (the `bin` field is a new package surface; manifest should reflect it)