# SXL Export Icons

`@sxl-studio/export-icons` — installable CLI for exporting icon assets from Figma with incremental diff, flexible multi-config, font generation, SF Symbols pipeline, and optional Bridge/MCP source mode.

## Install

Requires Node.js 22.12 or newer (`^22.12.0 || >=24`).

```bash
npm install --save-dev @sxl-studio/export-icons
```

## Environment and secrets

Create `.env` in your repository root and provide your Figma credentials there:

```bash
FIGMA_TOKEN=figd_xxxxxxxxxxxxxxxxx
SXL_ICONS_FILE_KEY=xxxxxxxxxxxx
```

Required Figma token scopes:

- `file_content:read`
- `file_metadata:read`

No write scopes are required for this CLI.

Add `.env` to `.gitignore` and never commit tokens:

```gitignore
.env
.env.local
```

## CLI commands

```bash
# If package is installed in repo (recommended)
pnpm exec sxl-export-icons init
pnpm exec sxl-export-icons init --wizard

# One-shot run without install
pnpm dlx @sxl-studio/export-icons init
npx @sxl-studio/export-icons init

# Validate one or many configs
pnpm exec sxl-export-icons validate-config --config sxl-export-icons.config.yaml
pnpm exec sxl-export-icons validate-config --config-glob "packages/*/sxl-export-icons*.yaml"

# Sync (default command)
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --adopt-existing
pnpm exec sxl-export-icons sync --config sxl-export-icons.config.yaml --target font-subset-local
pnpm exec sxl-export-icons --config sxl-export-icons.config.yaml --dry-run
pnpm exec sxl-export-icons --config sxl-export-icons.config.yaml --full
pnpm exec sxl-export-icons --config-glob "packages/*/sxl-export-icons*.yaml" --report .reports/icons-sync.json

# Enable fallback if source mode=mcp failed
pnpm exec sxl-export-icons sync --config ./icons.yaml --allow-fallback-rest

# Clean generated files from state/config scopes
pnpm exec sxl-export-icons clean --config ./icons.yaml
pnpm exec sxl-export-icons clean --config ./icons.yaml --dry-run

# Print state coverage report
pnpm exec sxl-export-icons report --config ./icons.yaml

# Probe Figma Motion runtime export support through Bridge/Remote Connect
pnpm exec sxl-export-icons probe-motion --format MP4,WEBM,GIF,SVG,SVG_STRING --inspect-only
pnpm exec sxl-export-icons probe-motion --node-id 1:2 --format MP4,WEBM,GIF,SVG --ignore-overlapping-layers --include-id-attribute
```

`--full` resets the complete state cache before applying any `--target` filter. Prefer a full run with all targets. With `--full --target <id>`, non-selected assets remain on disk when `pruneUntracked: false`, but their next sync is recalculated from an empty state and reports them as new.

> Do not use `pnpx sxl-export-icons ...` in registries with private proxy mapping.
> `pnpx`/`dlx` can try to resolve unscoped package `sxl-export-icons` and return 404.

## Running from a project terminal

```bash
cd ./packages/design-system/tokens

# First run: adopt already downloaded files as baseline state
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml --adopt-existing

# Regular sync
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml

# Local subset conversion only
pnpm exec sxl-export-icons sync --config ../sxl-export-icons.config.yaml --target font-subset-local
```

Notes:

- CLI auto-loads `.env.local` / `.env` from current and parent folders.
- `--adopt-existing` is bootstrap mode for empty scope state.
- During REST source loading, CLI shows live progress stages/pages
  (`REST 1/3`, `REST 2/3`, `REST 3/3`) so long runs are observable.

## Config v3

Canonical filename: `sxl-export-icons.config.yaml`.

Legacy config `version: 2` (`icons.config.yaml`) is supported via auto-migration.
When v2 config is loaded, relative paths are resolved from the current workspace root to keep legacy behavior.

```yaml
version: 3

env:
  figmaTokenVar: FIGMA_TOKEN

bridge:
  baseUrl: http://127.0.0.1:37830
  authTokenVar: BRIDGE_AUTH_TOKEN

state:
  file: .sxl/cache/icons-state.json

safety:
  allowOutsideWorkspace: false

sources:
  - id: ds-icons
    mode: rest # rest | mcp
    fileKeyVar: SXL_ICONS_FILE_KEY
    pageName: Icons
    downloadSpeed: 20
    descriptionExportMarker:
      key: sxl-studio-export-icon
      includeWhenMissing: true
    selectors:
      includeComponentSetNames: []
      includeComponentNames: []
      variantMatch:
        style: outline

targets:
  - id: web-svg
    mode: sync # sync | convert-local
    sourceIds: [ds-icons]
    download:
      enabled: true
      pruneUntracked: false
    output:
      path: assets/icons/svg
      format: svg # static: svg | png | jpg | webp | gif | pdf; motion.enabled: svg | gif | mp4 | webm
      scale: 1
      quality: 100
      lossless: false
      unsafeSvgPolicy: fail # fail | embed-image-fills | embed-png
      qualitySize: null
      width: null
      height: null
      modeWH: null # null | cover | proportion | fill
    naming:
      case: kebab # kebab | snake | camel | pascal
      separator: "-"
      pattern: "{prefix}{variant.style}{sectionName}{baseName}{suffix}"
      prefix: null
      suffix: null
      includeSectionName: false
      collisionStrategy: add-nodeid # error | add-nodeid | add-index
    font:
      enabled: true
      path: assets/icons/font
      formats: [woff2, woff, ttf, eot, svg]
      fontName: icons
      engine: webfonts-generator # webfonts-generator | advanced
      includeNames: [] # optional subset for font/SF generation
      excludeNames: [] # optional exclusion list
    sfSymbols:
      enabled: true
      path: assets/icons/ios
    overrides:
      - name: illustrations-lossless-webp-128
        match:
          sectionName: Illustrations
        patch:
          output:
            format: webp
            width: 128
            height: 128
            quality: 100
            lossless: true
          sourceExport:
            transport: plugin
            format: png
            constraint:
              type: WIDTH
              value: 128
```

## Source mode: `rest` and `mcp`

- `mode: rest` — direct Figma REST API.
- `mode: mcp` — read via SXL Bridge/Remote Connect (`/api/command` with page/tree traversal).
- If `mode: mcp` fails and `--allow-fallback-rest` is passed, CLI auto-falls back to REST for sources with resolved `fileKey`.

The `bridge` block is used for `mode: mcp` sources, `motion.enabled` targets, and static items whose effective `sourceExport.transport` is `plugin`. A static REST source does not use Bridge unless its target or matching override selects Plugin transport.

## SVG safety for embedded image fills

`output.unsafeSvgPolicy` controls static SVG export when the indexed Figma structure contains a visible `IMAGE` fill inside a `VECTOR` node:

- `fail` is the default. A structural preflight stops the current config before its output and state writes. Fix the source when the result must remain fully vector.
- `embed-image-fills` is an opt-in REST-only repair for `source.mode: rest` with `sourceExport.transport: rest` or the default transport. Export Icons keeps the Figma REST SVG and embeds only IMAGE/STRETCH fills missing from it, using image URLs returned by `GET /files/:key/images` and references from `fillOverrideTable`. Plugin, Bridge, and Remote Connect are not required.
- `embed-png` keeps the explicit Plugin full-node PNG contract. It requires effective `sourceExport.transport: plugin` and `sourceExport.format: png`, then embeds the full rendered node in an SVG wrapper.

The REST repair accepts only an unambiguous mapping to one opaque IMAGE/STRETCH paint with an invertible transform. Unsupported or mixed paints, effects, masks, non-STRETCH modes, ambiguous SVG path mapping, missing metadata, and Figma file version races fail closed before repaired output is committed. Normal SVGs without missing image fills keep the Figma REST SVG unchanged.

```yaml
targets:
  - id: media-svg
    sourceIds: [icons]
    output:
      path: assets/icons/media-svg
      format: svg
      unsafeSvgPolicy: embed-image-fills
```

`--dry-run` describes this path as `REST SVG + embedded IMAGE fill repair` and reports the number of image regions. The repaired SVG embeds raster image fills, so it cannot be used for icon-font generation or SF Symbols.

The legacy `embed-png` path remains available when a full-node Plugin render is explicitly required.

Plugin-backed SVG is always represented as embedded PNG even when structural risk metadata is absent. `sourceExport.constraint` is optional and defaults effectively to `SCALE:1`; when authored, it controls the Plugin PNG pixel dimensions. `output.scale` does not control this SVG path. The wrapper `width`, `height`, and `viewBox` come from indexed logical render bounds: `absoluteRenderBounds` first, then `absoluteBoundingBox`. Position changes do not alter the item fingerprint, but logical resize does. Missing, non-finite, or non-positive bounds fail preflight before Plugin SVG output. Because the content is raster, it can blur when enlarged. JPG, Motion, icon-font, and SF Symbols combinations are rejected; use a separate target without vector-only auxiliary pipelines.

The current MCP page/tree payload does not guarantee those absolute bounds. Export Icons preserves real `absoluteRenderBounds` or `absoluteBoundingBox` when the payload contains them, but it does not reinterpret local `width`/`height` as absolute bounds. For the raster-backed SVG path, use `mode: rest` to index the Figma file and `sourceExport.transport: plugin` to obtain the PNG bytes. An MCP-indexed item without real absolute bounds fails preflight.

```yaml
targets:
  - id: media-raster-backed-svg
    sourceIds: [icons]
    selectors:
      includeComponentNames: [media-preview]
    output:
      path: assets/icons/media-svg
      format: svg
      unsafeSvgPolicy: embed-png
    sourceExport:
      transport: plugin
      format: png
      constraint:
        type: SCALE
        value: 4
```

For `embed-png`, `--dry-run` identifies every indexed asset that resolves to the embedded-PNG representation and prints Plugin transport, PNG source format, constraint, and logical dimensions. When no constraint was authored, dry-run shows its effective Plugin default as `SCALE:1`. The sync JSON report records only items planned after diffing under `sourceExport.plugin` and `svgExport.embeddedPng`, together with `rasterBacked: true`.

For `mode: mcp` with REST-backed static downloads, the CLI also reads the exact SVG node metadata and file version through Figma REST before export. This requires the configured Figma token and `fileKey`/`fileKeyVar`; the sync fails instead of treating missing paint metadata as safe.

## Static raster export through Plugin

`sourceExport` is optional at target level and in `overrides[].patch`. It controls how Figma supplies the intermediate static raster:

```yaml
sourceExport:
  transport: plugin # rest | plugin
  format: png # png | jpg
  constraint:
    type: WIDTH # SCALE | WIDTH | HEIGHT
    value: 128 # positive; SXL limit: SCALE <= 16, WIDTH/HEIGHT <= 4096
```

When omitted, transport remains `rest`. `output.scale` remains the REST export scale and accepts values from `0.01` through `4`.

Plugin transport:

- supports static `png`, `jpg`, `webp`, and `gif` outputs, plus only the explicit raster-backed SVG tuple shown above; it cannot be combined with `motion.enabled`;
- calls Figma node `exportAsync` through the current compatible SXL Studio Plugin, Bridge, and Remote Connect session;
- requires the configured Figma file to be open;
- requires `fileKey`/`fileKeyVar` on the source, including `mode: mcp`, and verifies that key against the active Plugin document before filesystem operations;
- exports a `png` or `jpg` intermediate using `SCALE`, `WIDTH`, or `HEIGHT`, then applies local output conversion;
- rejects local upscaling: the Plugin intermediate must already contain enough pixels for the requested output size.

In Community Plugin builds, `figma.fileKey` may be unavailable. In that case open **Repository connection** in SXL Studio, set **Figma file URL or key**, and save it once for this document. This fallback compares the configured key with document plugin data; it is a manual document assertion, not an independent Figma identity lookup. Re-save the correct key after duplicating a file. Sync fails safely when the identity is absent or does not match. A single sync may use Plugin transport for only one Figma file; split multi-file exports by config or `--target`.

SXL safety limits are `SCALE <= 16`, `WIDTH/HEIGHT <= 4096`, projected raster sides no larger than 4096 px, a 16 million pixel render budget, and 16 MiB of encoded PNG/JPG bytes per node. Nodes without finite render bounds are rejected before `exportAsync`. These are SXL memory/transport limits, not documented Figma API limits. Plugin intermediates are staged and published only after the Plugin batch succeeds, so a disconnect does not replace earlier outputs in that batch; the following filesystem commit is sequential rather than atomic.

For example, this override takes a square 24x24 vector node from the `Illustrations` section, asks Figma for a 128x128 PNG, and writes a lossless WebP that remains 128x128:

```yaml
overrides:
  - name: illustrations-lossless-webp-128
    match:
      sectionName: Illustrations
    patch:
      output:
        format: webp
        width: 128
        height: 128
        quality: 100
        lossless: true
      sourceExport:
        transport: plugin
        format: png
        constraint:
          type: WIDTH
          value: 128
```

`output.lossless` defaults to `false`. For WebP, `true` keeps Sharp in lossless encoding, including when `qualitySize` is checked; the CLI does not silently switch to lossy output. This describes the encoding mode, not metadata or byte-for-byte identity with the Plugin intermediate. Embedded bitmaps cannot gain detail that is missing in their source pixels.

`--dry-run` prints both the pre-diff SVG safety candidate inventory described above and the number and names of Plugin exports planned after diffing. A sync JSON report includes only each planned Plugin node, intermediate format, and authored constraint (`null` when omitted). Effective output and source-export settings are hashed per item, so changing an override re-exports matching items without forcing unrelated matches to export again.

## Motion animated icon export

Animated icons authored with Figma Motion can be exported by normal `sync` when a target explicitly enables `motion.enabled`.

Supported Motion output formats:

- `svg` -> animated SVG generated from Motion keyframes;
- `gif` -> native Figma Motion GIF export through the plugin runtime;
- `mp4` -> native Figma Motion MP4 export through the plugin runtime;
- `webm` -> native Figma Motion WebM export through the plugin runtime.

Motion export requires:

- SXL Studio Bridge running;
- SXL Studio plugin launched in the same Figma file with Remote Connect enabled;
- a `rest` source with `fileKey`/`fileKeyVar` for normal file indexing, or an `mcp` source when the file is already open in Figma.

```yaml
targets:
  - id: motion-webm
    sourceIds: [default-source]
    output:
      path: assets/motion
      format: webm
      scale: 1
      quality: 90
    motion:
      enabled: true
      fps: 30
      loop: true
      ignoreOverlappingLayers: true
      includeIdAttribute: true
```

`motion.enabled` is deliberately opt-in. Static targets keep the existing REST/SVG/raster export path. Motion targets are re-exported on every sync because Figma Motion keyframes are read through the plugin runtime and are not present in the REST file JSON fingerprint.

Figma's public Plugin typings/docs do not expose MP4/WebM/GIF Motion formats, but current Figma Motion runtime accepts native `exportAsync` formats `MP4`, `WEBM`, and `GIF`. Export Icons uses that native path for media formats and uses plugin `SVG_STRING` plus Motion keyframes for animated SVG. `ignoreOverlappingLayers` maps to `contentsOnly` for SVG/SVG_STRING; the current native Motion media runtime rejects that key, so GIF/MP4/WebM use Figma runtime defaults for overlap handling.

## Motion runtime probe

`probe-motion` is a diagnostic command for checking what the current Figma plugin runtime exposes for Figma Motion nodes.

It requires SXL Studio Bridge and an active Remote Connect session in Figma. Without `--node-id`, it probes the current Figma selection. It returns Motion metadata, available `figma.motion` runtime keys, existing export settings, and optional `exportAsync` probe results without writing files.

```bash
pnpm exec sxl-export-icons probe-motion \
  --format MP4,WEBM,GIF,SVG,SVG_STRING \
  --ignore-overlapping-layers \
  --include-id-attribute
```

Useful options:

- `--inspect-only` — read metadata without calling `exportAsync`;
- `--format <list>` — comma-separated or repeatable formats to probe;
- `--ignore-overlapping-layers` — uses `contentsOnly: true` for `SVG` / `SVG_STRING`; native Motion media formats use Figma runtime defaults;
- `--include-descendants` — includes descendant Motion keyframes used by animated icon export;
- `--include-id-attribute` — passes `svgIdAttribute: true` for `SVG` / `SVG_STRING`.

## Target mode: `sync` and `convert-local`

- `mode: sync` (default) — reads Figma sources, downloads/updates assets, then runs aux pipelines.
- `mode: convert-local` — does not use Figma sources; reads existing local SVG files from `output.path` and runs font/SF pipelines.

Rules for `convert-local`:

- `sourceIds` must be empty;
- `download.enabled` must be `false`;
- `output.format` must be `svg`;
- at least one of `font.enabled` or `sfSymbols.enabled` must be `true`.

To run only a local-conversion target from mixed config:

```bash
pnpm exec sxl-export-icons sync --config ./sxl-export-icons.config.yaml --target font-subset-local
```

## Description marker filter

You can control export in Figma description of `COMPONENT` / `COMPONENT_SET`:

```
sxl-studio-export-icon: true
```

or

```
sxl-studio-export-icon: false
```

Config:

```yaml
sources:
  - id: ds-icons
    descriptionExportMarker:
      key: sxl-studio-export-icon
      includeWhenMissing: true
```

Rules:

- marker `true` -> include
- marker `false` -> exclude
- marker missing -> use `includeWhenMissing`

## What is exported

- Only `COMPONENT` nodes.
- For `COMPONENT_SET`, exports each `COMPONENT` variant child as a separate item.
- Variant matching works with arbitrary property names (for example `styleMode`, `platform`, `state`, ...), not only `style`.
- If source sets `sectionName`/`sectionId`, items outside that section are skipped.

## One or multiple formats

- `target.output.format` is a single format per target.
- To export the same source in multiple formats, add multiple targets with shared `sourceIds`.

```yaml
targets:
  - id: icons-svg
    sourceIds: [ds-icons]
    output:
      path: assets/icons/svg
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
  - id: icons-webp
    sourceIds: [ds-icons]
    output:
      path: assets/icons/webp
      format: webp
      scale: 1
      quality: 90
      qualitySize: null
      width: null
      height: null
      modeWH: null
```

## Incremental diff behavior

Scope identity: `sourceId::targetId`.

- `NEW` — node appears first time in scope.
- `UPDATED` — `updatedAt`/fingerprint changes, file missing, format changed, or effective render/export settings changed.
- `DELETED` — node removed from source scope.
- `RENAMED` — canonical name/path changes while format stays same.
- `UNCHANGED` — no content and no config-impact changes.

State file stores scope metadata, config hash, and exported item hashes.
For REST sources, node fingerprints are computed from render-relevant node data (`geometry=paths`), so pure vector-shape edits are detected even when metadata timestamps stay unchanged.

### Moving a checkout or worktree

State and generated assets can be copied together into another checkout with the same paths relative to the workspace root (the CLI working directory). Starting with 1.1.7, item render hashes exclude the output destination. Scope hashes retain output, font and SF Symbols destinations relative to the workspace, including overrides. An output move remains a `RENAMED` item rather than a render change; scale, quality, format and source-export changes still invalidate matching items.

Absolute `motion.ffmpegPath` values inside the workspace are hashed relative to it. Executables outside the workspace and bare commands such as `ffmpeg` retain their identity. Explicit external destinations also remain absolute. The same rules apply to overrides. Missing output files still require export.

State stays at version 3. On a successful sync, an exact match against the 1.1.6 hashes computed from the current settings is upgraded to portable hashes without re-exporting unchanged items. Run this first sync in the original checkout before moving an old state. If the old state has already moved, its original absolute paths cannot be recovered from hashes; unverifiable item settings are re-exported once rather than assumed unchanged. `--adopt-existing` remains bootstrap-only and does not bypass this check for a populated scope.

Bootstrap behavior (first run without scope state):

- every matched icon is treated as `NEW` and re-exported;
- this guarantees replacement/update even when files already exist on disk.
- with `--adopt-existing`, existing files are adopted as `UNCHANGED` baseline instead of forced re-download.

Optional strict cleanup:

- set `download.pruneUntracked: true` to remove files with the same target format
  in `output.path` that are not present in current Figma scope.
- enable it only for dedicated output directories, because cleanup is path+format based.

## Output policies

- Paths are resolved relative to config file location.
- Export outside workspace is blocked by default.
- To allow external absolute/relative destinations set:

```yaml
safety:
  allowOutsideWorkspace: true
```

## Post-processing

- `svg` -> SVGO optimization
- `png`/`jpg` -> Sharp optimization + optional resize
- `webp` -> export PNG from Figma, then convert to WebP
- static `gif` -> export PNG from Figma, then convert to GIF
- Motion `svg` -> export SVG with node IDs and apply Motion keyframes as CSS animation
- Motion `gif`/`mp4`/`webm` -> native Figma Motion export through Bridge/Remote Connect
- `pdf` -> direct Figma export

## Convert-only targets (no download)

If icons already exist locally and you only need font/SF conversion:

```yaml
targets:
  - id: icons-font-only
    mode: convert-local
    sourceIds: []
    download:
      enabled: false
    output:
      path: assets/icons/svg
      format: svg
      scale: 1
      quality: 100
      qualitySize: null
      width: null
      height: null
      modeWH: null
    naming:
      case: kebab
      separator: "-"
      pattern: "{variant.style}{baseName}"
      includeSectionName: false
      collisionStrategy: add-nodeid
    font:
      enabled: true
      path: assets/icons/font
      formats: [woff2, woff, ttf, eot]
      fontName: ds-icons
      includeNames: [filled-home, outline-home]
```

Notes:

- `download.enabled: false` skips file mutation (no download/rename/delete);
- `font.includeNames` limits conversion to a selected subset;
- names in `font.includeNames` / `excludeNames` can be provided with or without `.svg`;
- source SVG files must already exist in `output.path`.

## Font pipeline

- Adapter-based interface with `webfonts-generator` default engine.
- Supports `woff2`, `woff`, `ttf`, `svg`, `eot`.
- Preserves stable codepoints (`codepoints.json`) between runs.
- Generates preview (`preview.html`) + stylesheet.

## SF Symbols pipeline

- Generates `Assets.xcassets/*.symbolset`.
- Uses `symbol-rendering-intent` in generated `Contents.json`.
- Rejects complex SVG features (gradients, filters, masks, embedded images).

## Multi-config usage

You can run many configs in one call:

```bash
pnpm exec sxl-export-icons sync \
  --config packages/ds-a/sxl-export-icons.config.yaml \
  --config packages/ds-b/sxl-export-icons.config.yaml \
  --report .reports/icons-mono.json
```

Or by glob:

```bash
pnpm exec sxl-export-icons sync --config-glob "packages/*/sxl-export-icons.config.yaml"
```

For multiple pages/sections from the same Figma file, define separate sources with the same `fileKey`/`fileKeyVar` and different `pageName`/`sectionName`.

## Naming token notes (`variant.*`)

- `variant` is a reserved token namespace in `naming.pattern`.
- `variant.<propName>` uses any Figma variant property name (for example `style`, `type`, `size`, `state`).
- Multi-prop naming is supported by chaining tokens:
  - `"{prefix}{pageName}{variant.style}{variant.type}{baseName}"`
- If a property is missing for a node, that segment is skipped.
  In mixed sets this can cause collisions, so use `collisionStrategy` and/or add disambiguators like `{sectionName}` or `{nodeId}`.

## Duplicate preflight (before download)

When duplicate output names are detected, CLI prints the full duplicate list (name + node id + variant props) and asks for action:

- `refresh` — re-read the source, then repeat SVG safety and Plugin readiness preflight before the refreshed scope resumes duplicate planning. Outputs from earlier completed scopes or configs are not rolled back if this refreshed preflight fails.
- `skip` — keep one node and skip duplicate entries
- `all` — download all duplicates using rename strategy (`add-nodeid` / `add-index`)
- `abort` — stop sync

Interactive TTY mode uses keyboard selection (`↑`/`↓` + `Enter`) instead of manual text input.

In non-interactive mode (CI), prompts are skipped and configured `collisionStrategy` is applied automatically.

## Troubleshooting

- `Missing Figma token` -> set env var from `env.figmaTokenVar`.
- `Path ... is outside workspace` -> enable `safety.allowOutsideWorkspace`.
- `Bridge session is not connected` -> start `utils/bridge`, open plugin, enable Remote Connect.
- `Naming collision detected` -> change `naming.pattern` or use `collisionStrategy: add-nodeid/add-index`.
- Too many 429 responses -> lower `downloadSpeed` for source.

## Development

In this monorepo the package lives in `utils/icons` (root script: `npm run icons:check`).

```bash
npm ci
npm run check   # ESLint + typecheck + Prettier + Vitest + tsc build
npm run dev -- --help
```

## Contract references used in implementation

- Figma REST: [`GET /v1/files/:key`](https://www.figma.com/developers/api#get-files-endpoint), [`GET /v1/images/:key`](https://www.figma.com/developers/api#get-images-endpoint), components/component_sets metadata endpoints.
- Sharp conversion/encoding docs for WebP/GIF/resize.
- `webfonts-generator` formats and options.

This utility keeps those contracts explicit in code and docs to reduce drift and regression risk.

## License

SXL Studio Free Use License, see [`LICENSE`](LICENSE): free to use, including commercially, and everything the tool generates is yours; the package is not open source and may not be redistributed or published in modified form. Versions published earlier under MIT stay under MIT.
