openpencil/packages/docs/reference/cli.md

300 lines
8.6 KiB
Markdown
Raw Normal View History

---
title: CLI Reference
description: Complete reference for all openpencil commands, options, and flags.
---
# CLI Reference
All commands accept a `.fig` file as a positional argument. When omitted, the CLI connects to the running desktop app via RPC.
## info
Show document info — pages, node counts, fonts, file size.
```sh
openpencil info [file] [--json]
```
| Option | Description |
|--------|-------------|
| `--json` | Output as JSON |
## tree
Print the node hierarchy.
```sh
openpencil tree [file] [options]
```
| Option | Description |
|--------|-------------|
| `--page` | Page name (default: first page) |
| `--depth` | Max depth (default: unlimited) |
| `--json` | Output as JSON |
## find
Search nodes by name or type.
```sh
openpencil find [file] [options]
```
| Option | Description |
|--------|-------------|
| `--name` | Node name (partial match, case-insensitive) |
| `--type` | Node type: `FRAME`, `TEXT`, `RECTANGLE`, `INSTANCE`, etc. |
| `--page` | Page name (default: all pages) |
| `--limit` | Max results (default: 100) |
| `--json` | Output as JSON |
## node
Show detailed properties of a node.
```sh
openpencil node [file] --id <id> [--json]
```
| Option | Description |
|--------|-------------|
| `--id` | **Required.** Node ID (e.g. `1:23`) |
| `--json` | Output as JSON |
## pages
List all pages in the document.
```sh
openpencil pages [file] [--json]
```
| Option | Description |
|--------|-------------|
| `--json` | Output as JSON |
## variables
List design variables and collections.
```sh
openpencil variables [file] [options]
```
| Option | Description |
|--------|-------------|
| `--collection` | Filter by collection name |
| `--type` | Filter by type: `COLOR`, `FLOAT`, `STRING`, `BOOLEAN` |
| `--json` | Output as JSON |
## export
feat: export components as Storybook stories (#751) * feat(cli): export components as Storybook stories Add `openpencil export -f storybook`, which writes one CSF3 `.stories.ts` file per component set or component. Each variant becomes a story and the variant properties become select controls, so the story renders the matching variant; an unknown combination throws instead of showing another variant. Stories embed the existing inline-style HTML projection, so consumers need no OpenPencil runtime. `--framework react|vue|html` only changes the render wrapper and the Meta/StoryObj import. When the document sits under the current directory, stories carry an `openpencil://` design link for @storybook/addon-designs. Refs #727 * fix(pen): size auto-width text from its content on import Text without a width in an auto-layout parent was imported 10000px wide, a placeholder the app's text measurer replaces. Headless layout keeps stored sizes, so CLI HTML and Storybook exports stretched hugging frames to over 10000px. Import the width as 0 so the importer's existing text-length estimate applies, and headless layout estimates the rest. * feat(app): follow layer links to other pages openpencil:// and web ?node= links only searched the current page, so a Storybook story linking to a component on another page reported it missing. When the current page has no match, load the other pages without showing them and switch to the first that carries the name. * feat(cli): add design images and watch mode to Storybook export Each story now links to its own variant when the layer name is unique, and carries a 2x PNG of the variant for @storybook/addon-designs, imported so Vite bundles it. --watch re-exports on every save. Re-exports replace the stories a previous export of the same document generated, including those of deleted components, and refuse to overwrite hand-written stories or another document's. Refs #727 * fix(cli): reference Storybook design images without ambient PNG types Import design images with new URL(..., import.meta.url) instead of an import declaration, so consumers need no vite/client types to typecheck the stories. Document that exports should run from the same directory. * fix(app): search other pages for a link without cancelling page switches The cross-page layer search prepared each page with preparePage, which advances the page-switch generation, so a page switch the user had in progress could be dropped, and every searched page paid for fonts and layout. Add loadPageNodes, which populates a page's layers through the same worker path without touching the switch generation, and report a failed search as an error instead of a missing layer. * fix(pen): never import width-less text zero wide Text without a width now imports at width 0 and relies on the importer's text-length estimate, which skipped single-glyph text. Estimate zero-width text of any length. * fix(cli): harden Storybook export ownership, titles, and links - A --page export replaces only its own stories, and names files as a full export does, so it cannot delete or overwrite other pages' stories. - Same-named components on a page get distinct titles, so Storybook story ids do not collide. - Read the generated header through CRLF line endings, and refuse a source containing a line break, which would end the header comment and start code. - Link a story only to a layer name no other layer carries. - Document the --page default for Storybook export. Refs #727 * fix(app): let a page switch overtake a link's layer search A link search that loads other pages could resume after the user started switching pages and move them to the matching page. Expose pageSwitchCount, which advances whenever a page switch starts, and abandon the search when it changes. An overtaken search reports neither a match nor a missing layer. * fix(pen): estimate only omitted text widths Estimate a width-less text node's width when it is imported, instead of estimating every zero-width text node afterwards, so an explicit width of 0 is kept. * fix(cli): track Storybook story ownership by document path and page - Identify the document by its path relative to the output directory rather than a basename or cwd-relative path, so same-named documents do not share stories and the export no longer depends on the working directory. - Record the page in each story's header; a --page export replaces all of that page's stories and asks for a full export when renumbered file names land on another page's. - Check every target, including design images, before removing anything, and refuse to overwrite files this export does not own. - Quote the header fields as JSON with U+2028/U+2029 escaped, so any path stays inside the comment, instead of refusing line breaks. - Deduplicate titles by Storybook id, which ignores case and punctuation. Refs #727 * fix(app): focus a searched page only after its switch committed A page switch the user starts while the link search's own switch is pending can keep that switch from committing. Check that the search's switch was the only one and landed on its page before focusing; otherwise report the search as superseded. * fix(pen): keep empty text without a width at zero * fix(cli): remove only the design images a Storybook export generated Replacing a story removed its whole .design folder, including files someone else put there. Read the images each owned story references, remove just those, and remove a .design folder only once it is empty. Refs #727 * test(app): cover a page switch still pending during a link search The previous test committed the overtaking switch, so the page check alone caught it. Advance the switch count without committing, so the test fails without the count check. * fix(cli): stage Storybook exports and refuse linked design folders - Write every file to a staging folder inside the output before removing the previous export, then move them into place, so a failed write no longer leaves the export half replaced. - Refuse a .design path that is not a real folder, such as a symbolic link, before removing or writing images through it, so an export cannot reach outside the output directory. Refs #727 * refactor(dom-css): print Storybook stories from a parsed template Story modules were assembled from string fragments, so quoting and layout were an implicit contract: the CLI found design images with a regex that only matched double-quoted `new URL("…")` paths. A story module is now one TypeScript template, parsed once with acorn and its TypeScript plugin. Data is filled into `$placeholder` nodes and the module is printed with esrap, which owns quoting and escaping. The CLI reads referenced design images back through `storyImagePaths()` instead of matching text. Tests import generated modules and assert values rather than formatting. * refactor(storybook): track generated files in a manifest The export recovered which files it owned by parsing its own output: a header regex over JSON-quoted strings, line-separator escaping, CRLF handling, an AST walk for design images, and a path regex in the CLI. A `.openpencil-stories.json` manifest now records the document and page behind each generated file. The CLI validates it with Valibot, including that every listed path stays inside the output folder, and the story header is a plain note. Story ids use a copy of Storybook's `sanitize`, tested against the installed Storybook; the previous rule treated `A§B` and `A-B` as the same story. Export names use es-toolkit's `pascalCase`. The CLI export command moves into `commands/export/`, dom-css splits grouping and naming out of the Storybook exporter, and the CLI takes the framework list from dom-css. * fix(pen): keep explicit narrow text widths A post-import pass widened every multi-character text narrower than two font sizes, including widths the `.pen` file set on purpose, such as `width: 0`. Omitted widths are now estimated when the text node is created, so the pass only overrode explicit widths and is removed. --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-09-29 23:16:47 +00:00
Export to PNG, JPG, WEBP, SVG, JSX, HTML, `.fig`, or Storybook stories.
```sh
openpencil export [file] [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
feat: export components as Storybook stories (#751) * feat(cli): export components as Storybook stories Add `openpencil export -f storybook`, which writes one CSF3 `.stories.ts` file per component set or component. Each variant becomes a story and the variant properties become select controls, so the story renders the matching variant; an unknown combination throws instead of showing another variant. Stories embed the existing inline-style HTML projection, so consumers need no OpenPencil runtime. `--framework react|vue|html` only changes the render wrapper and the Meta/StoryObj import. When the document sits under the current directory, stories carry an `openpencil://` design link for @storybook/addon-designs. Refs #727 * fix(pen): size auto-width text from its content on import Text without a width in an auto-layout parent was imported 10000px wide, a placeholder the app's text measurer replaces. Headless layout keeps stored sizes, so CLI HTML and Storybook exports stretched hugging frames to over 10000px. Import the width as 0 so the importer's existing text-length estimate applies, and headless layout estimates the rest. * feat(app): follow layer links to other pages openpencil:// and web ?node= links only searched the current page, so a Storybook story linking to a component on another page reported it missing. When the current page has no match, load the other pages without showing them and switch to the first that carries the name. * feat(cli): add design images and watch mode to Storybook export Each story now links to its own variant when the layer name is unique, and carries a 2x PNG of the variant for @storybook/addon-designs, imported so Vite bundles it. --watch re-exports on every save. Re-exports replace the stories a previous export of the same document generated, including those of deleted components, and refuse to overwrite hand-written stories or another document's. Refs #727 * fix(cli): reference Storybook design images without ambient PNG types Import design images with new URL(..., import.meta.url) instead of an import declaration, so consumers need no vite/client types to typecheck the stories. Document that exports should run from the same directory. * fix(app): search other pages for a link without cancelling page switches The cross-page layer search prepared each page with preparePage, which advances the page-switch generation, so a page switch the user had in progress could be dropped, and every searched page paid for fonts and layout. Add loadPageNodes, which populates a page's layers through the same worker path without touching the switch generation, and report a failed search as an error instead of a missing layer. * fix(pen): never import width-less text zero wide Text without a width now imports at width 0 and relies on the importer's text-length estimate, which skipped single-glyph text. Estimate zero-width text of any length. * fix(cli): harden Storybook export ownership, titles, and links - A --page export replaces only its own stories, and names files as a full export does, so it cannot delete or overwrite other pages' stories. - Same-named components on a page get distinct titles, so Storybook story ids do not collide. - Read the generated header through CRLF line endings, and refuse a source containing a line break, which would end the header comment and start code. - Link a story only to a layer name no other layer carries. - Document the --page default for Storybook export. Refs #727 * fix(app): let a page switch overtake a link's layer search A link search that loads other pages could resume after the user started switching pages and move them to the matching page. Expose pageSwitchCount, which advances whenever a page switch starts, and abandon the search when it changes. An overtaken search reports neither a match nor a missing layer. * fix(pen): estimate only omitted text widths Estimate a width-less text node's width when it is imported, instead of estimating every zero-width text node afterwards, so an explicit width of 0 is kept. * fix(cli): track Storybook story ownership by document path and page - Identify the document by its path relative to the output directory rather than a basename or cwd-relative path, so same-named documents do not share stories and the export no longer depends on the working directory. - Record the page in each story's header; a --page export replaces all of that page's stories and asks for a full export when renumbered file names land on another page's. - Check every target, including design images, before removing anything, and refuse to overwrite files this export does not own. - Quote the header fields as JSON with U+2028/U+2029 escaped, so any path stays inside the comment, instead of refusing line breaks. - Deduplicate titles by Storybook id, which ignores case and punctuation. Refs #727 * fix(app): focus a searched page only after its switch committed A page switch the user starts while the link search's own switch is pending can keep that switch from committing. Check that the search's switch was the only one and landed on its page before focusing; otherwise report the search as superseded. * fix(pen): keep empty text without a width at zero * fix(cli): remove only the design images a Storybook export generated Replacing a story removed its whole .design folder, including files someone else put there. Read the images each owned story references, remove just those, and remove a .design folder only once it is empty. Refs #727 * test(app): cover a page switch still pending during a link search The previous test committed the overtaking switch, so the page check alone caught it. Advance the switch count without committing, so the test fails without the count check. * fix(cli): stage Storybook exports and refuse linked design folders - Write every file to a staging folder inside the output before removing the previous export, then move them into place, so a failed write no longer leaves the export half replaced. - Refuse a .design path that is not a real folder, such as a symbolic link, before removing or writing images through it, so an export cannot reach outside the output directory. Refs #727 * refactor(dom-css): print Storybook stories from a parsed template Story modules were assembled from string fragments, so quoting and layout were an implicit contract: the CLI found design images with a regex that only matched double-quoted `new URL("…")` paths. A story module is now one TypeScript template, parsed once with acorn and its TypeScript plugin. Data is filled into `$placeholder` nodes and the module is printed with esrap, which owns quoting and escaping. The CLI reads referenced design images back through `storyImagePaths()` instead of matching text. Tests import generated modules and assert values rather than formatting. * refactor(storybook): track generated files in a manifest The export recovered which files it owned by parsing its own output: a header regex over JSON-quoted strings, line-separator escaping, CRLF handling, an AST walk for design images, and a path regex in the CLI. A `.openpencil-stories.json` manifest now records the document and page behind each generated file. The CLI validates it with Valibot, including that every listed path stays inside the output folder, and the story header is a plain note. Story ids use a copy of Storybook's `sanitize`, tested against the installed Storybook; the previous rule treated `A§B` and `A-B` as the same story. Export names use es-toolkit's `pascalCase`. The CLI export command moves into `commands/export/`, dom-css splits grouping and naming out of the Storybook exporter, and the CLI takes the framework list from dom-css. * fix(pen): keep explicit narrow text widths A post-import pass widened every multi-character text narrower than two font sizes, including widths the `.pen` file set on purpose, such as `width: 0`. Omitted widths are now estimated when the text node is created, so the pass only overrode explicit widths and is removed. --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-09-29 23:16:47 +00:00
| `--format` | `-f` | `png` (default), `jpg`, `webp`, `svg`, `pdf`, `pptx`, `jsx`, `tailwind-jsx`, `html`, `fig`, `storybook` |
| `--output` | `-o` | Output file path (default: `<name>.<format>`); a directory for `storybook` (default: `<name>-stories`) |
| `--scale` | `-s` | Export scale (default: 1) |
| `--quality` | `-q` | Quality 0–100, JPG/WEBP only (default: 90) |
feat: export components as Storybook stories (#751) * feat(cli): export components as Storybook stories Add `openpencil export -f storybook`, which writes one CSF3 `.stories.ts` file per component set or component. Each variant becomes a story and the variant properties become select controls, so the story renders the matching variant; an unknown combination throws instead of showing another variant. Stories embed the existing inline-style HTML projection, so consumers need no OpenPencil runtime. `--framework react|vue|html` only changes the render wrapper and the Meta/StoryObj import. When the document sits under the current directory, stories carry an `openpencil://` design link for @storybook/addon-designs. Refs #727 * fix(pen): size auto-width text from its content on import Text without a width in an auto-layout parent was imported 10000px wide, a placeholder the app's text measurer replaces. Headless layout keeps stored sizes, so CLI HTML and Storybook exports stretched hugging frames to over 10000px. Import the width as 0 so the importer's existing text-length estimate applies, and headless layout estimates the rest. * feat(app): follow layer links to other pages openpencil:// and web ?node= links only searched the current page, so a Storybook story linking to a component on another page reported it missing. When the current page has no match, load the other pages without showing them and switch to the first that carries the name. * feat(cli): add design images and watch mode to Storybook export Each story now links to its own variant when the layer name is unique, and carries a 2x PNG of the variant for @storybook/addon-designs, imported so Vite bundles it. --watch re-exports on every save. Re-exports replace the stories a previous export of the same document generated, including those of deleted components, and refuse to overwrite hand-written stories or another document's. Refs #727 * fix(cli): reference Storybook design images without ambient PNG types Import design images with new URL(..., import.meta.url) instead of an import declaration, so consumers need no vite/client types to typecheck the stories. Document that exports should run from the same directory. * fix(app): search other pages for a link without cancelling page switches The cross-page layer search prepared each page with preparePage, which advances the page-switch generation, so a page switch the user had in progress could be dropped, and every searched page paid for fonts and layout. Add loadPageNodes, which populates a page's layers through the same worker path without touching the switch generation, and report a failed search as an error instead of a missing layer. * fix(pen): never import width-less text zero wide Text without a width now imports at width 0 and relies on the importer's text-length estimate, which skipped single-glyph text. Estimate zero-width text of any length. * fix(cli): harden Storybook export ownership, titles, and links - A --page export replaces only its own stories, and names files as a full export does, so it cannot delete or overwrite other pages' stories. - Same-named components on a page get distinct titles, so Storybook story ids do not collide. - Read the generated header through CRLF line endings, and refuse a source containing a line break, which would end the header comment and start code. - Link a story only to a layer name no other layer carries. - Document the --page default for Storybook export. Refs #727 * fix(app): let a page switch overtake a link's layer search A link search that loads other pages could resume after the user started switching pages and move them to the matching page. Expose pageSwitchCount, which advances whenever a page switch starts, and abandon the search when it changes. An overtaken search reports neither a match nor a missing layer. * fix(pen): estimate only omitted text widths Estimate a width-less text node's width when it is imported, instead of estimating every zero-width text node afterwards, so an explicit width of 0 is kept. * fix(cli): track Storybook story ownership by document path and page - Identify the document by its path relative to the output directory rather than a basename or cwd-relative path, so same-named documents do not share stories and the export no longer depends on the working directory. - Record the page in each story's header; a --page export replaces all of that page's stories and asks for a full export when renumbered file names land on another page's. - Check every target, including design images, before removing anything, and refuse to overwrite files this export does not own. - Quote the header fields as JSON with U+2028/U+2029 escaped, so any path stays inside the comment, instead of refusing line breaks. - Deduplicate titles by Storybook id, which ignores case and punctuation. Refs #727 * fix(app): focus a searched page only after its switch committed A page switch the user starts while the link search's own switch is pending can keep that switch from committing. Check that the search's switch was the only one and landed on its page before focusing; otherwise report the search as superseded. * fix(pen): keep empty text without a width at zero * fix(cli): remove only the design images a Storybook export generated Replacing a story removed its whole .design folder, including files someone else put there. Read the images each owned story references, remove just those, and remove a .design folder only once it is empty. Refs #727 * test(app): cover a page switch still pending during a link search The previous test committed the overtaking switch, so the page check alone caught it. Advance the switch count without committing, so the test fails without the count check. * fix(cli): stage Storybook exports and refuse linked design folders - Write every file to a staging folder inside the output before removing the previous export, then move them into place, so a failed write no longer leaves the export half replaced. - Refuse a .design path that is not a real folder, such as a symbolic link, before removing or writing images through it, so an export cannot reach outside the output directory. Refs #727 * refactor(dom-css): print Storybook stories from a parsed template Story modules were assembled from string fragments, so quoting and layout were an implicit contract: the CLI found design images with a regex that only matched double-quoted `new URL("…")` paths. A story module is now one TypeScript template, parsed once with acorn and its TypeScript plugin. Data is filled into `$placeholder` nodes and the module is printed with esrap, which owns quoting and escaping. The CLI reads referenced design images back through `storyImagePaths()` instead of matching text. Tests import generated modules and assert values rather than formatting. * refactor(storybook): track generated files in a manifest The export recovered which files it owned by parsing its own output: a header regex over JSON-quoted strings, line-separator escaping, CRLF handling, an AST walk for design images, and a path regex in the CLI. A `.openpencil-stories.json` manifest now records the document and page behind each generated file. The CLI validates it with Valibot, including that every listed path stays inside the output folder, and the story header is a plain note. Story ids use a copy of Storybook's `sanitize`, tested against the installed Storybook; the previous rule treated `A§B` and `A-B` as the same story. Export names use es-toolkit's `pascalCase`. The CLI export command moves into `commands/export/`, dom-css splits grouping and naming out of the Storybook exporter, and the CLI takes the framework list from dom-css. * fix(pen): keep explicit narrow text widths A post-import pass widened every multi-character text narrower than two font sizes, including widths the `.pen` file set on purpose, such as `width: 0`. Omitted widths are now estimated when the text node is created, so the pass only overrode explicit widths and is removed. --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-09-29 23:16:47 +00:00
| `--page` | | Page name (default: first page; `fig`, `pptx`, and `storybook` default to every page) |
| `--node` | | Node ID to export (default: all top-level nodes) |
refactor!: register HTML and Tailwind JSX as IO formats (#774) * refactor!: register HTML and Tailwind JSX as IO formats HTML and Tailwind JSX went around the IO registry: the CLI appended `html` to its format list and had its own HTML and Tailwind export paths, so the app's export options offered neither. Core now registers `html` and `tailwind-jsx` adapters built on a new browser-safe `@open-pencil/dom-css/export` entry. Export results can carry assets written next to the main file, which covers standalone HTML with external images and fonts, and the CLI writes every format the same way. The CSS object model and Node file access load only when an export needs them, so the app bundle stays free of the headless CSS runtime. BREAKING CHANGE: `sceneNodesToTailwindJSX` and `designDocumentToTailwindJSX` moved from `@open-pencil/dom-css/browser` to `@open-pencil/dom-css/export`. * refactor(core): share export support and fixed-size options across IO formats Five adapters export every target and six have no scale or quality options; the new HTML and Tailwind JSX adapters repeated those blocks again. Both are now named once and shared. * fix(core): keep HTML asset paths relative for Windows output paths The CLI passed the absolute output path as the export file name, and the HTML adapter only split it on `/`, so on Windows the page referenced absolute `C:\...\card.assets` paths and assets were written to a doubled location. The CLI now passes the file name, and the adapter accepts either separator.
2026-09-26 07:54:16 +00:00
| `--style` | | JSX style: `openpencil` (default), `tailwind` (same as `-f tailwind-jsx`) |
| `--html` | | HTML mode: `fragment` (default), `standalone` |
| `--css` | | HTML CSS output: `inline` (default), `tailwind` |
| `--assets` | | Standalone HTML assets: `inline` (default), `external` |
| `--fonts` | | Standalone HTML font output: `assets`, `none` (default) |
feat: export components as Storybook stories (#751) * feat(cli): export components as Storybook stories Add `openpencil export -f storybook`, which writes one CSF3 `.stories.ts` file per component set or component. Each variant becomes a story and the variant properties become select controls, so the story renders the matching variant; an unknown combination throws instead of showing another variant. Stories embed the existing inline-style HTML projection, so consumers need no OpenPencil runtime. `--framework react|vue|html` only changes the render wrapper and the Meta/StoryObj import. When the document sits under the current directory, stories carry an `openpencil://` design link for @storybook/addon-designs. Refs #727 * fix(pen): size auto-width text from its content on import Text without a width in an auto-layout parent was imported 10000px wide, a placeholder the app's text measurer replaces. Headless layout keeps stored sizes, so CLI HTML and Storybook exports stretched hugging frames to over 10000px. Import the width as 0 so the importer's existing text-length estimate applies, and headless layout estimates the rest. * feat(app): follow layer links to other pages openpencil:// and web ?node= links only searched the current page, so a Storybook story linking to a component on another page reported it missing. When the current page has no match, load the other pages without showing them and switch to the first that carries the name. * feat(cli): add design images and watch mode to Storybook export Each story now links to its own variant when the layer name is unique, and carries a 2x PNG of the variant for @storybook/addon-designs, imported so Vite bundles it. --watch re-exports on every save. Re-exports replace the stories a previous export of the same document generated, including those of deleted components, and refuse to overwrite hand-written stories or another document's. Refs #727 * fix(cli): reference Storybook design images without ambient PNG types Import design images with new URL(..., import.meta.url) instead of an import declaration, so consumers need no vite/client types to typecheck the stories. Document that exports should run from the same directory. * fix(app): search other pages for a link without cancelling page switches The cross-page layer search prepared each page with preparePage, which advances the page-switch generation, so a page switch the user had in progress could be dropped, and every searched page paid for fonts and layout. Add loadPageNodes, which populates a page's layers through the same worker path without touching the switch generation, and report a failed search as an error instead of a missing layer. * fix(pen): never import width-less text zero wide Text without a width now imports at width 0 and relies on the importer's text-length estimate, which skipped single-glyph text. Estimate zero-width text of any length. * fix(cli): harden Storybook export ownership, titles, and links - A --page export replaces only its own stories, and names files as a full export does, so it cannot delete or overwrite other pages' stories. - Same-named components on a page get distinct titles, so Storybook story ids do not collide. - Read the generated header through CRLF line endings, and refuse a source containing a line break, which would end the header comment and start code. - Link a story only to a layer name no other layer carries. - Document the --page default for Storybook export. Refs #727 * fix(app): let a page switch overtake a link's layer search A link search that loads other pages could resume after the user started switching pages and move them to the matching page. Expose pageSwitchCount, which advances whenever a page switch starts, and abandon the search when it changes. An overtaken search reports neither a match nor a missing layer. * fix(pen): estimate only omitted text widths Estimate a width-less text node's width when it is imported, instead of estimating every zero-width text node afterwards, so an explicit width of 0 is kept. * fix(cli): track Storybook story ownership by document path and page - Identify the document by its path relative to the output directory rather than a basename or cwd-relative path, so same-named documents do not share stories and the export no longer depends on the working directory. - Record the page in each story's header; a --page export replaces all of that page's stories and asks for a full export when renumbered file names land on another page's. - Check every target, including design images, before removing anything, and refuse to overwrite files this export does not own. - Quote the header fields as JSON with U+2028/U+2029 escaped, so any path stays inside the comment, instead of refusing line breaks. - Deduplicate titles by Storybook id, which ignores case and punctuation. Refs #727 * fix(app): focus a searched page only after its switch committed A page switch the user starts while the link search's own switch is pending can keep that switch from committing. Check that the search's switch was the only one and landed on its page before focusing; otherwise report the search as superseded. * fix(pen): keep empty text without a width at zero * fix(cli): remove only the design images a Storybook export generated Replacing a story removed its whole .design folder, including files someone else put there. Read the images each owned story references, remove just those, and remove a .design folder only once it is empty. Refs #727 * test(app): cover a page switch still pending during a link search The previous test committed the overtaking switch, so the page check alone caught it. Advance the switch count without committing, so the test fails without the count check. * fix(cli): stage Storybook exports and refuse linked design folders - Write every file to a staging folder inside the output before removing the previous export, then move them into place, so a failed write no longer leaves the export half replaced. - Refuse a .design path that is not a real folder, such as a symbolic link, before removing or writing images through it, so an export cannot reach outside the output directory. Refs #727 * refactor(dom-css): print Storybook stories from a parsed template Story modules were assembled from string fragments, so quoting and layout were an implicit contract: the CLI found design images with a regex that only matched double-quoted `new URL("…")` paths. A story module is now one TypeScript template, parsed once with acorn and its TypeScript plugin. Data is filled into `$placeholder` nodes and the module is printed with esrap, which owns quoting and escaping. The CLI reads referenced design images back through `storyImagePaths()` instead of matching text. Tests import generated modules and assert values rather than formatting. * refactor(storybook): track generated files in a manifest The export recovered which files it owned by parsing its own output: a header regex over JSON-quoted strings, line-separator escaping, CRLF handling, an AST walk for design images, and a path regex in the CLI. A `.openpencil-stories.json` manifest now records the document and page behind each generated file. The CLI validates it with Valibot, including that every listed path stays inside the output folder, and the story header is a plain note. Story ids use a copy of Storybook's `sanitize`, tested against the installed Storybook; the previous rule treated `A§B` and `A-B` as the same story. Export names use es-toolkit's `pascalCase`. The CLI export command moves into `commands/export/`, dom-css splits grouping and naming out of the Storybook exporter, and the CLI takes the framework list from dom-css. * fix(pen): keep explicit narrow text widths A post-import pass widened every multi-character text narrower than two font sizes, including widths the `.pen` file set on purpose, such as `width: 0`. Omitted widths are now estimated when the text node is created, so the pass only overrode explicit widths and is removed. --------- Co-authored-by: Danila Poyarkov <dev@dannote.net>
2026-09-29 23:16:47 +00:00
| `--framework` | | Storybook framework: `react` (default), `vue`, `html` |
| `--design-images` | | Storybook: render a PNG per variant for the Design panel (default: on; `--no-design-images` to skip) |
| `--watch` | | Storybook: re-export whenever the document is saved |
| `--beside` | | Storybook: write each document's stories into the document's own folder; the file argument can then be several files or a quoted glob |
| `--thumbnail` | | Export page thumbnail instead of full render |
| `--width` | | Thumbnail width (default: 1920) |
| `--height` | | Thumbnail height (default: 1080) |
## import
2026-06-03 17:36:27 +00:00
Import HTML/CSS/Tailwind into an editable OpenPencil document.
2026-06-03 17:36:27 +00:00
```sh
openpencil import page.html [options]
2026-06-03 17:36:27 +00:00
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--format` | `-f` | Output format: `fig` (default), `json` |
2026-06-03 17:36:27 +00:00
| `--output` | `-o` | Output file path (default: `<name>.<format>`) |
| `--css` | | CSS file to apply before conversion |
| `--css-text` | | Inline CSS text to apply before conversion |
| `--tailwind` | | Tailwind utility candidates to compile and apply |
| `--tailwind-file` | | File containing Tailwind utility candidates |
| `--page-name` | | Scene graph page name (default: `DOM/CSS`) |
| `--json` | | Print a machine-readable summary |
Examples:
```sh
openpencil import card.html --css card.css -o card.fig
openpencil import card.html --tailwind "flex flex-col gap-3 w-80 p-6 rounded-xl bg-white" -o card.fig
2026-06-03 17:36:27 +00:00
```
## eval
Execute JavaScript with the Figma Plugin API.
```sh
openpencil eval [file] [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--code` | `-c` | JavaScript code to execute |
| `--stdin` | | Read code from stdin |
| `--write` | `-w` | Write changes back to the input file |
| `--output` | `-o` | Write to a different file |
| `--json` | | Output as JSON |
| `--quiet` | `-q` | Suppress output |
## analyze colors
Analyze color palette usage across the document.
```sh
openpencil analyze colors [file] [options]
```
| Option | Description |
|--------|-------------|
| `--limit` | Max colors to show (default: 30) |
| `--threshold` | Distance threshold for clustering similar colors, 0–50 (default: 15) |
| `--similar` | Show similar color clusters |
| `--json` | Output as JSON |
## analyze typography
Analyze font family, size, and weight distribution.
```sh
openpencil analyze typography [file] [options]
```
| Option | Description |
|--------|-------------|
| `--group-by` | Group by: `family`, `size`, `weight` (default: show all styles) |
| `--limit` | Max styles to show (default: 30) |
| `--json` | Output as JSON |
## analyze spacing
Analyze gap and padding values across auto-layout frames.
```sh
openpencil analyze spacing [file] [options]
```
| Option | Description |
|--------|-------------|
| `--grid` | Base grid size to check against (default: 8) |
| `--json` | Output as JSON |
## analyze clusters
Find repeated node patterns — potential components.
```sh
openpencil analyze clusters [file] [options]
```
| Option | Description |
|--------|-------------|
| `--limit` | Max clusters to show (default: 20) |
| `--min-size` | Min node size in px (default: 30) |
| `--min-count` | Min instances to form a cluster (default: 2) |
| `--json` | Output as JSON |
feat: add visual diff and patch apply tools and openpencil diff (#810) * feat(core): add visual diff and patch apply tools diff_visual renders two nodes at one scale through the existing raster export, compares them with pixelmatch, and returns the diff PNG with the changed ratio and region in source-node coordinates. It takes export_image's scale and maxEdge inputs. FigmaAPI gains a CanvasKit-backed raster codec and a pageId export option, so the app and headless CLI decode pixels and render nodes off the current page. diff_apply applies diff_create and diff_show patches through the Figma API, validates every node before changing any, and supports dryRun and force. diff_show now simulates changes on a detached copy with the same property code. One serializer and parser back all three. diffDocuments compares two documents page by page by name path. Image tool results now reach models as media with their metadata as text, for any tool rather than export_image alone. diff_create, diff_jsx, and diff_visual join the default AI tool set, and the diff tools are no longer hidden from WebMCP. * feat(cli): add diff commands and agent diff guidance openpencil diff create, jsx, show, apply, and visual run the Core diff tools on a file or the running app; apply writes back with --write or --output like eval. diff files compares two documents page by page and exits 1 when they differ. The chat prompt asks the agent to edit in place and to verify risky edits against a reference copy with diff_jsx, diff_create, and diff_visual. The skill, CLI reference, MCP tool table, and a new Comparing Designs page document the commands and tools. * feat(core): diff and patch node trees as JSX attributes diff_create, diff_show, diff_apply, and diffDocuments used a hand-rolled `key: value` property format that covered about fifteen properties, matched children by name path, and could not see moves. Nodes are now projected to the attributes the JSX export prints, and jsondiffpatch matches children (by ID or by name path) and detects moves. Patches list `-`/`+` attribute lines per node plus moved, added, and removed children. diff_apply checks every hunk first, applies attribute changes through the renderer's prop handling, and changes only the fields an attribute moves, so IDs, instance links, and other state survive. diff_show takes JSX attributes instead of a JSON props object. design-jsx gains sceneNodeAttributes, parseJSXAttributes, and jsxNodeFields for this, and the export round-trip property table is shared so every case is also diffed and applied. `diff files` loads its documents in order so node IDs, and so its patches, are deterministic. * fix(core): keep diff_apply atomic and diff files honest about differences - Added nodes render before anything else changes; if one fails, for example on a missing component, the rendered ones are deleted and nothing else is committed. - A hunk with an attribute the renderer ignores fails instead of reporting "unchanged". - diffDocuments reports `changed` from page statuses, and a page only one document has gets its status but no patch, since patches do not add or remove pages. diff files uses it, so an added empty page no longer reads as a match. - diff files rejects a --page neither document has and a --depth that is not a non-negative integer, exiting 2; diff_create's depth is validated the same way.
2026-10-03 17:00:13 +00:00
## diff create
Patch that turns one node tree into another, as JSX attribute changes plus moved, added, and removed children. Children match by name; see [Comparing designs](/programmable/cli/comparing) for the format.
```sh
openpencil diff create [file] --from <id> --to <id> [options]
```
| Option | Description |
|--------|-------------|
| `--from` | Source node ID |
| `--to` | Target node ID |
| `--depth` | Max tree depth (default: 10) |
| `--json` | Output as JSON |
## diff jsx
Structural diff between two nodes as design JSX.
```sh
openpencil diff jsx [file] --from <id> --to <id> [--json]
```
## diff show
Preview the patch that setting JSX attributes on a node would produce, without changing it.
```sh
openpencil diff show <id> [file] --attributes '<jsx attributes>' [--json]
```
`--attributes` takes attributes as the JSX export writes them, such as `'w={200} bg="#FF0000"'`.
## diff apply
Apply a patch from `diff create`, `diff show`, or `diff files`. Every node must still match the patch's old values unless `--force` is set, and nothing changes unless every hunk applies.
```sh
openpencil diff apply <patch> [file] [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--dry-run` | | Validate and list changes without applying |
| `--force` | | Apply even when current values differ from the patch |
| `--write` | `-w` | Write changes back to the input file |
| `--output` | `-o` | Write to a different file |
| `--json` | | Output as JSON |
Pass `-` as the patch path to read it from stdin.
## diff visual
Pixel diff between two rendered nodes, written as a PNG with changed pixels in red.
```sh
openpencil diff visual [file] --from <id> --to <id> --output <png> [options]
```
| Option | Alias | Description |
|--------|-------|-------------|
| `--output` | `-o` | Diff PNG path |
| `--scale` | | Render scale before the max-edge limit (default: 1) |
| `--max-edge` | | Maximum image width or height (default: 1280) |
| `--threshold` | | Color tolerance 0–1; smaller is stricter (default: 0.1) |
| `--json` | | Output as JSON |
## diff files
Structural diff of two documents, page by page. Pages match by name and nodes by name path, so two versions of a file compare even though their node IDs differ. Exits with status 1 when the documents differ and 2 when the options are invalid, such as a `--page` neither document has.
```sh
openpencil diff files <before> <after> [options]
```
| Option | Description |
|--------|-------------|
| `--page` | Compare only the page with this name |
| `--depth` | Max tree depth below each page (default: unlimited) |
| `--json` | Output as JSON |