8.9 KiB
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. (Seeopenspec/AGENTS.mdSection 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.jsonis parsed - THEN
bin["suaveplan-font-compress"] === "./dist/bin/suaveplan-font-compress.js"
Scenario: shebang preserved in compiled output
- WHEN
dist/bin/suaveplan-font-compress.jsis read afterbunx 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 is0AND no FS operations are performed (verified by spy onnode: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')).versionAND the exit code is0
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 is2
Scenario: conflicting recursive flags exit 2
- WHEN the bin is invoked with
["./fonts", "--recursive", "--no-recursive"] - THEN the exit code is
2AND 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
.ttffile path - THEN
FontConverter.convertis called once ANDBinaryFontPacker.packis called once with the converter's output AND a.btffile is written next to the input (or under--outputif specified)
Scenario: JSON input runs pack only
- WHEN the bin is invoked with a single
.jsonfile path containing a validTypefaceFont - THEN
FontConverter.convertis not called ANDBinaryFontPacker.packis called once with the parsed JSON AND a.btffile is written
Scenario: directory input uses BatchConverter
- WHEN the bin is invoked with a directory path containing two
.ttffiles at--recursive - THEN
BatchConverteris called once with the directory ANDBinaryFontPacker.packis called twice AND two.btffiles 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
.ttfinput and--format=json - THEN a
.jsonfile is written ANDBinaryFontPacker.packis not called AND the JSON parses as a validTypefaceFont
Scenario: default format is btf
- WHEN the bin is invoked without
--format - THEN
--format=btfis the effective default AND.btffiles 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 successfully1— 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
3AND 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
1AND three.btffiles 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_CODESis 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
FontCompressErroris thrown withcode === "FONT_COMPRESS.CLI_INVALID_ARGS"ANDcontext.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: 0AND a compression-ratio line
Verified by: src/bin/integration/cli-roundtrip.test.ts::"per-file progress lines and summary block printed"