Initial Commit
This commit is contained in:
@@ -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 -->
|
||||
Reference in New Issue
Block a user