mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
An experimental, best-effort `.fig` export, so work done in FigCraft can be handed back as a `.fig` file. It is written from the same public format write-ups the importer uses and is the inverse of the importer's mapping. Only round trips through FigCraft's own importer are verified; other apps may not open every file.
- **Encoder** (`crates/format/src/fig/kiwi.rs`): the kiwi encoder that test fixtures used is now shipped code. It never panics and checks every value against the schema: unknown fields or enum names, missing struct fields, wrong types, out-of-range numbers and non-finite floats are errors, and NULs inside strings are dropped. Varints and floats are written exactly the way the decoder reads them. `Schema::parse` reads the compact text schema form, and the importer's tests use it too.
- **Writer** (`crates/format/src/fig/export.rs`): writes the zip (`canvas.fig`, `meta.json`, `thumbnail.png`, `images/<hash>`). `canvas.fig` holds the fig-kiwi header, then a raw-deflate binary schema and a raw-deflate message (flate2's pure-Rust backend, already in the tree; no new dependencies). The schema covers only the fields we write. The canvas, images, blobs, node count and nesting depth are capped, and floats are made finite before they are written.
- **Mapping:** pages; frames, groups, sections, rectangles, ellipses (arcs), polygons, stars, lines and vectors (vector network blobs with per-point radius, mirroring and caps, plus fill outlines); text with styled runs (UTF-16 indexed) and list types; slices; boolean groups; solid, gradient and image paints (image bytes included); strokes; effects; corners; constraints; masks; auto layout, including grids with tracks and cells; child layout; layout grids; export settings; components, component sets and their properties. Instances are written as their main plus the fields their layers change, by id path. This covers nested instances and swaps, and fields the main sets but the instance clears. Prototype interactions (triggers, navigation and overlay actions, transitions, easing), scrolling and flow starts are included. Everything skipped is reported in a warnings list: style links, variables, comments, Dev Mode data, plugin data, guides, slots, brush and width-profile strokes, pattern fills, image adjustments, variable and conditional actions, and more.
- **Import fixes found by the round trips** (`map.rs`): ellipse arcs, boolean operation types and component set descriptions are now read. Durations are read as whole milliseconds, because seconds stored as 32-bit floats used to truncate by 1 ms.
- **File writes:** a failed write no longer leaves its `.tmp-figcraft` file behind. The junk-parameter sweep found this.
- **Commands and UI:**
- `file.export {format: "fig"}`, or any path ending in `.fig`, writes the whole file and returns `warnings`. Without a path it returns base64 `data`.
- File > Export... has a new "The whole file" section with a ".fig (experimental)" row. A note under it says other apps may not open every exported file. After export, a toast reports how many kinds of content were left out.
- `figcraft-cli export --format fig` (or `-o x.fig`) and `convert` to `.fig` print the warnings as `note:` lines.
- MCP's `export` accepts `format: "fig"` and passes the warnings through.
- New UI strings have English and Spanish entries.
- **Docs:** file-format-parity.md has a new export section covering what is written, what is skipped, and that only round trips through our own importer are verified. Also updated: mcp.md, cli.md, gaps.md (gap 1), target-app-parity.md and ROADMAP.md. Grading is kept low. Import/export goes to 86%/58% and file formats to 60%. The file-exchange discount stays at x0.85, because nothing shows that another app opens these files.
134 lines
7.2 KiB
Markdown
134 lines
7.2 KiB
Markdown
# figcraft-cli
|
||
|
||
FigCraft without a window: inspect, export, convert and render documents, run command scripts, list
|
||
commands, serve MCP and measure performance. Build it with
|
||
`cargo build --release -p figcraft-cli` (binary `target/release/figcraft-cli`).
|
||
|
||
Bad arguments print usage and exit with code 2; failures exit with 1. The CLI never panics on input.
|
||
|
||
The collaboration server is a binary of its own, `figcraft-server` (`cargo build --release -p
|
||
figcraft-server`): it keeps files with share links and serves their live sessions, and its
|
||
`key`, `files` and `links` subcommands manage them. See [server.md](server.md).
|
||
|
||
## Subcommands
|
||
|
||
### `info <file> [--json]`
|
||
|
||
Pages, node counts, top-level frames (id, type, name, size), style/variable/image counts. Repairs
|
||
of a damaged file and import warnings go to stderr as `note:` lines (`notes` with `--json`).
|
||
|
||
Every subcommand that reads a file also takes the public REST API file JSON (the response of
|
||
`GET /v1/files/:key` or of its nodes endpoint, written to a `.json` file): it is imported on the fly.
|
||
Image fills come from `images/<imageRef>.png` (or `.jpg`, …) next to the JSON when present, else a
|
||
grey placeholder. `.fig` files are read the same way (best effort, images from the file itself);
|
||
`info` lists what wasn't imported as `note:` lines.
|
||
|
||
```text
|
||
$ figcraft-cli info app.figcraft
|
||
app.figcraft — App (412 nodes, 6 styles, 12 variables, 3 images)
|
||
page 1 "Screens": 380 nodes, 4 top-level
|
||
2 FRAME "Home" 393.0×852.0
|
||
…
|
||
```
|
||
|
||
### `export <file> -o <out> [--format png|jpg|svg|webp|pdf|fig] [--scale N] [--page NAME] [--node ID|NAME] [--bleed LENGTH] [--crop-marks] [--bleed-marks] [--cmyk]`
|
||
|
||
- `<out>` is a directory: one file per top-level frame of the page (default the first page), named
|
||
after the frame (`Home.png`, `Settings.png`…); with no frames, one file per top-level layer.
|
||
- `<out>` has an image extension: one file with the given `--node`, or the whole page composited.
|
||
- `--node` takes a layer id (`12` or `0:12`) or a layer name.
|
||
- `--format fig` (or an `<out>` ending in `.fig`) writes the whole file as `.fig`, experimental:
|
||
written from public format notes and checked only by opening it again in FigCraft; other apps
|
||
may not open every file. What was left out (shared style links, variables, comments, Dev Mode
|
||
data…) is printed as `note:` lines.
|
||
- The format comes from `--format`, else `<out>`'s extension, else png. `--scale` (default 1, max 64)
|
||
applies to raster formats.
|
||
- A `.pdf` `<out>` without `--node` holds the page's top-level frames, one PDF page each in reading
|
||
order, each the frame's size in points (a page without frames goes on one page). Shapes, text
|
||
and gradients stay vector, text is real (selectable, with subset fonts) and its links clickable;
|
||
shadows and blurs are images.
|
||
- PDF print options: `--bleed` (points, or a length like `3mm` or `0.125in`, up to 2 in) grows each
|
||
page by the bleed, which shows what lies past the frame's edge (its fills reach out to it, its
|
||
children are clipped there instead); `--crop-marks` and `--bleed-marks` draw hairline marks
|
||
outside the bleed; the page boxes are set (MediaBox the sheet, BleedBox, TrimBox the frame).
|
||
`--cmyk` writes CMYK colour, images included, converted from sRGB with the naive formula
|
||
`K = 1 − max(R, G, B)`, `C = (1 − R − K) / (1 − K)` (M and Y alike): DeviceCMYK without an
|
||
output intent (no CMYK profile ships with FigCraft), so proof against your printer's profile.
|
||
|
||
```sh
|
||
figcraft-cli export app.figcraft -o exports/ --scale 2
|
||
figcraft-cli export app.figcraft -o hero.svg --node "Hero"
|
||
figcraft-cli export app.figcraft -o screens.pdf --page "Screens"
|
||
figcraft-cli export app.figcraft -o poster.pdf --node "Poster" --bleed 3mm --crop-marks --cmyk
|
||
figcraft-cli export app.figcraft -o handoff.fig
|
||
```
|
||
|
||
### `convert <in> <out>`
|
||
|
||
By extension. Inputs: `.figcraft`, `.json` (FigCraft or REST API file JSON), `.fig` (best effort), `.svg` (an editable frame: groups,
|
||
shapes, text, gradients, masks and effects; `info` lists what it left out as `note:` lines), `.png/.jpg/.webp/.gif/.tif/.bmp`
|
||
(an image layer). Outputs: `.figcraft`, `.json` (FigCraft JSON, pretty-printed), `.svg`,
|
||
`.png/.jpg/.webp` (the current page rendered at 1x with its background), `.pdf` (the current page's
|
||
frames, one page each), `.fig` (the whole file, experimental, as `export --format fig`).
|
||
|
||
### `render <file> -o <out.png> [--scale N] [--page NAME]`
|
||
|
||
The whole page (default the first) with its background, as png, jpg or webp. Large pages are scaled
|
||
down to fit 16384 px.
|
||
|
||
### `run <script.json|script.jsonl> [--in <file>] [--out <file>]`
|
||
|
||
Runs commands in a headless session (a new document, or `--in`), printing one JSON line per step:
|
||
|
||
```text
|
||
{"command":"frame.create","ok":true,"result":{"id":2},"step":0}
|
||
```
|
||
|
||
A failing step prints `"ok": false` with the error and exits with code 1 (nothing is saved).
|
||
`--out` saves the result by extension like `convert`.
|
||
|
||
Script formats: a JSON array of `{"command": id, "params": {...}}`, an object
|
||
`{"commands": [...]}`, or JSON lines (one step per line; blank lines and `//` lines are skipped).
|
||
|
||
```json
|
||
[
|
||
{"command": "frame.create", "params": {"preset": "iPhone 16", "x": 0, "y": 0, "name": "Home"}},
|
||
{"command": "node.create", "params": {"type": "text", "parent": 2, "x": 24, "y": 64, "text": "Welcome", "props": {"fontSize": 32}}},
|
||
{"command": "layout.add", "params": {"ids": [2], "direction": "vertical"}}
|
||
]
|
||
```
|
||
|
||
Ids are assigned in order (a new document's first page is 1, so the first layer is 2), which lets a
|
||
script refer to layers it created.
|
||
|
||
### `commands [--json] [--filter TEXT]`
|
||
|
||
Every command with its label, shortcut, undoable flag (`*`) and parameter docs; `--json` for
|
||
machines.
|
||
|
||
### `mcp [--attach PORT]`
|
||
|
||
Serve MCP on stdio: headless by default, or attached to a running app started with
|
||
`figcraft --control PORT`. See [mcp.md](mcp.md). The headless session can host, join and publish
|
||
live sessions (`collab.*`) and use plugin directories (`plugin.directory.*`); the one-shot `run`
|
||
can't (no network link).
|
||
|
||
### `plugin publish <folder> --server ws://host:port/[?key=KEY]`
|
||
|
||
Publishes a plugin folder (`figcraft-plugin.json`, the module its `main` names, an optional `assets/`
|
||
folder) to a `figcraft-server`'s plugin directory as a new version, and prints its name, version,
|
||
size and SHA-256. The server's key comes from the address or, better, from the
|
||
`FIGCRAFT_SERVER_KEY` environment variable (it then stays out of the shell history and process
|
||
lists); it is never printed. The bundle is checked before it is sent and again by the server. See
|
||
[plugins.md](plugins.md#plugin-directories).
|
||
|
||
### `perf [--layers N[,N...]]`
|
||
|
||
Builds synthetic documents of each size (default `1000,10000` layers: screens with an auto layout
|
||
header, a card group, button instances and a grid of shapes and text) and prints a Markdown table of
|
||
the hot paths: build + normalise, save/load, 1440×900 renders at 100% and zoomed to fit, hit testing
|
||
and hover, single-layer edits through the engine (fill, move, text in auto layout, a main component
|
||
with all its instances), undo/redo, a drag preview frame, `normalize` after a one-node edit,
|
||
`document.inspect` and select-all + move. Use a release build for meaningful numbers;
|
||
`cargo xtask bench` runs it at 1k/10k/50k layers together with the UI frame benchmarks.
|