Sync specs & docs with master: eval command, app menu, autosave, unified tools

This commit is contained in:
Anton A S 2026-03-01 16:03:50 +03:00
parent 0a12925233
commit c6d70b501b
25 changed files with 2683 additions and 4 deletions

View file

@ -32,6 +32,7 @@ The root app (`src/`) is the Tauri/Vite desktop editor. Its `src/engine/` files
- `bun open-pencil analyze typography <file>` — font/size/weight stats
- `bun open-pencil analyze spacing <file>` — gap/padding values
- `bun open-pencil analyze clusters <file>` — repeated patterns
- `bun open-pencil eval <file> --code '<js>'` — execute JS with Figma Plugin API
## CLI
@ -106,6 +107,7 @@ The root app (`src/`) is the Tauri/Vite desktop editor. Its `src/engine/` files
- Number input spinner hiding is global CSS in `app.css`, not per-component
- ScrubInput (drag-to-change number) — cursor and pointerdown on outer container, not inner spans
- Icons: use unplugin-icons with Iconify/Lucide (`<icon-lucide-*>`) — don't use raw SVG or Unicode symbols
- App menu (`src/components/AppMenu.vue`) — browser-only menu bar using reka-ui Menubar components; Tauri uses native menus, so menu is hidden when `IS_TAURI` is true
- Sections are draggable by title pill, not by the area to the right of the title
- CSS `contain: paint layout style` on side panels to isolate repaints from WebGL canvas

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-01

View file

@ -0,0 +1,109 @@
## Context
OpenSpec and VitePress documentation fell out of sync after a major merge from master (commits 63748ec through 20290f6). The merge introduced:
- Figma Plugin API (`packages/core/src/figma-api.ts`, 1119 LOC) mirroring Figma's plugin surface for headless scripting
- Eval CLI command (`packages/cli/src/commands/eval.ts`) enabling JavaScript execution against .fig files
- Unified tool definitions (`packages/core/src/tools/`) centralizing AI/CLI/MCP tool schemas
- App menu for browser mode (`src/components/AppMenu.vue`, 196 LOC)
- Autosave with 3-second debounce (`src/stores/editor.ts`)
- Extensive test coverage (2571 LOC across 5 new test files)
- Comprehensive eval command documentation (`docs/eval-command.md`, 437 LOC)
These changes are implemented and tested but lack corresponding OpenSpec specs and comprehensive VitePress doc updates (especially comparison matrices). Current state creates documentation debt and risks divergence between code and specs.
**Constraints:**
- Specs live in `openspec/specs/` (main specs) and `openspec/changes/*/specs/` (delta specs during active changes)
- Documentation lives in `docs/` as a VitePress site (not a separate package, just a directory with `.vitepress/config.ts`)
- Changes are already implemented — this is a **documentation sync** task, not a feature build
- Must preserve existing spec structure and documentation style
## Goals / Non-Goals
**Goals:**
- Create OpenSpec specs for 4 new capabilities (figma-plugin-api, eval-command, app-menu, autosave)
- Update 4 existing capability specs (cli, tooling, testing, vitepress-docs) with delta specs
- Update Figma comparison matrix (`docs/guide/figma-comparison.md`) to reflect new features
- Update Penpot comparison (`docs/guide/comparison.md`) to highlight headless scripting
- Ensure specs match implemented behavior (read code/tests as source of truth)
- Link specs to VitePress docs where appropriate
**Non-Goals:**
- Changing code implementation (code is already merged and tested)
- Creating new VitePress articles (eval-command.md already exists)
- Restructuring spec organization or schemas
- Backward-compatibility analysis (all changes are additive)
## Decisions
### **Decision 1: Documentation is sync, not discovery**
**Choice:** Treat code/tests as source of truth; specs document implemented behavior.
**Rationale:**
- Changes already merged, tested (2571 LOC of tests), and documented (eval-command.md)
- Spec creation here is retrospective documentation, not forward-looking requirements
- Reading implementation reveals actual behavior > guessing from commit messages
**Alternatives considered:**
- Write specs from scratch based on desired behavior → Wrong: code is already shipped
- Skip specs entirely → Violates OpenSpec discipline, creates technical debt
### **Decision 2: Separate specs for each new capability**
**Choice:** Create 4 new capability specs (figma-plugin-api, eval-command, app-menu, autosave) in `specs/<name>/spec.md`.
**Rationale:**
- Each is independently testable and documented
- Figma Plugin API is a 1119-line implementation with 924 LOC of tests — deserves standalone spec
- Aligns with OpenSpec principle: one capability = one spec
**Alternatives considered:**
- Merge figma-plugin-api and eval-command into single spec → No: API is usable beyond eval (e.g., AI tools)
- Put app-menu and autosave into existing editor-ui spec → No: loses discoverability
### **Decision 3: Delta specs for modified capabilities**
**Choice:** Create delta specs in `openspec/changes/sync-specs-docs-with-master/specs/<capability>/spec.md` for cli, tooling, testing, vitepress-docs.
**Rationale:**
- Requirements changed (CLI has new eval subcommand, testing has 5 new test suites)
- Delta specs eventually merge into main specs during archive phase
- Preserves change history and review trail
### **Decision 4: Update comparison docs to reflect eval + app menu**
**Choice:**
- Update `docs/guide/figma-comparison.md` → add rows for app menu (Interface section), eval command (Plugin API section), update AI tools row
- Update `docs/guide/comparison.md` → add paragraph in Architecture section highlighting headless scripting advantage over Penpot
**Rationale:**
- App menu brings OpenPencil closer to Figma's UI parity (updates Interface & Navigation section)
- Eval command with Figma Plugin API is unique differentiator (Penpot has no plugin API)
- Comparison matrices are user-facing landing page content → must stay current
**Alternatives considered:**
- Skip comparison updates → No: comparison.md is linked from README and landing page
- Only update Figma comparison → No: headless scripting is architectural advantage over Penpot too
### **Decision 5: Minimal AGENTS.md updates**
**Choice:** Update AGENTS.md only to reflect tool unification (reference `packages/core/src/tools/` instead of `src/ai/tools.ts`).
**Rationale:**
- AGENTS.md already mentions tool architecture
- Eval command, app menu, autosave don't change agent instructions
- Unified tool definitions reduce duplication → agents reference canonical source
## Risks / Trade-offs
**[Risk: Specs may drift from code again]** → Mitigation: Add note to CONTRIBUTING.md or AGENTS.md to update specs alongside code
**[Risk: Verbose specs for retrospective documentation]** → Mitigation: Focus on requirements/scenarios, not rehashing code structure
**[Trade-off: Delta specs add file count]** → Acceptable: OpenSpec workflow requires deltas for modified capabilities; they merge during archive
## Migration Plan
N/A — documentation-only changes. No deployment, rollback, or runtime impact.
## Open Questions
None — all implementation details are observable in merged code and tests.

View file

@ -0,0 +1,42 @@
## Why
After merging changes from master, OpenSpec specs and VitePress documentation are out of sync with the codebase. Major new features (Figma Plugin API, eval command, app menu, autosave) and architectural improvements (unified tool definitions) are implemented but not documented in specs or user-facing docs. This creates a gap between code reality and documentation, reducing project maintainability and developer understanding.
## What Changes
- Add specs for Figma Plugin API (1119-line implementation mirroring Figma's plugin surface)
- Add specs for `eval` CLI command (headless scripting with `--code`, `--stdin`, `--write`)
- Add specs for app menu bar (browser mode: File, Edit, View, Object, Text, Arrange)
- Add specs for autosave (debounced write 3s after scene changes)
- Update CLI spec with eval command integration
- Update tooling spec with unified tool definition system (define once, adapt for AI/CLI/MCP)
- Update testing spec with app menu and autosave integration tests
- Add VitePress documentation for eval command (comprehensive guide with examples)
- Update AGENTS.md references to reflect new tool architecture
- Update Figma comparison matrix (docs/guide/figma-comparison.md) to reflect new capabilities (app menu, CLI eval command, Figma Plugin API compatibility)
- Update Penpot comparison (docs/guide/comparison.md) to highlight headless scripting advantage
## Capabilities
### New Capabilities
- `figma-plugin-api`: Figma-compatible Plugin API for headless scripting. Provides `figma` global object with node creation, manipulation, querying, and serialization matching Figma's surface. Includes SceneNode proxy wrappers, property getters/setters, type guards, and JSON export.
- `eval-command`: CLI command `open-pencil eval <file> --code '<js>'` for executing JavaScript against .fig files. Supports stdin input, writing changes back, and returning JSON results. Enables batch operations, AI tool execution, and headless testing.
- `app-menu`: Browser mode menu bar with File (New, Open, Save, Export), Edit (Undo, Redo, Cut, Copy, Paste), View (Zoom controls, Rulers), Object (Group, Frame, Component), Text (Font size, Bold, Align), and Arrange (Bring to Front, Send to Back) menus. Only visible in browser (`!IS_TAURI`).
- `autosave`: Automatic file saving with 3-second debounce after last scene change. Watches `sceneVersion`, uses `useDebounceFn` from VueUse, writes via Tauri or File System Access API. Disabled for new unsaved files (no fileHandle).
### Modified Capabilities
- `cli`: Add eval command to CLI suite. Integrates FigmaAPI execution environment, supports `--write`/`-o` for persisting changes, `--stdin` for multiline scripts, `--json` for structured output.
- `tooling`: Unified tool definitions in `packages/core/src/tools/`. Define tools once in `schema.ts`, adapt for AI (`ai-adapter.ts`), CLI (citty commands), and MCP (future). Deduplicates 311 lines from `src/ai/tools.ts` by using `FigmaAPI.toJSON()` and shared color parsing.
- `testing`: Add integration tests for app menu (`tests/e2e/app-menu.spec.ts`, 131 lines) and autosave (`tests/e2e/autosave.spec.ts`, 113 lines). Add eval CLI tests (`tests/engine/eval-cli.test.ts`, 202 lines), FigmaAPI tests (`tests/engine/figma-api.test.ts`, 924 lines), and tool adapter tests (409 lines across 3 files).
- `vitepress-docs`: Add `docs/eval-command.md` (437 lines) covering eval architecture, FigmaAPI surface, usage examples, AI integration, testing patterns, and migration from Figma plugins.
## Impact
- **Core:** New `packages/core/src/figma-api.ts` (1119 lines), `packages/core/src/tools/` (3 files, 936 lines)
- **CLI:** New `packages/cli/src/commands/eval.ts` (78 lines)
- **Editor:** New `src/components/AppMenu.vue` (196 lines), modified `src/stores/editor.ts` (autosave logic)
- **Tests:** 5 new test files (2571 lines total)
- **Docs:** New `docs/eval-command.md`, updated AGENTS.md
- **Breaking:** None (additive changes only)

View file

@ -0,0 +1,205 @@
## ADDED Requirements
### Requirement: App menu bar for browser mode
The system SHALL display a menu bar in browser mode (`!IS_TAURI`) with File, Edit, View, Object, Text, and Arrange menus.
#### Scenario: Menu visibility in browser
- **WHEN** app runs in browser (not Tauri desktop)
- **THEN** system displays menu bar at top of window
#### Scenario: Menu hidden in Tauri
- **WHEN** app runs in Tauri desktop mode
- **THEN** system hides menu bar (Tauri provides native menus)
### Requirement: File menu
The system SHALL provide File menu with Open, Save, Save As, and Export actions.
#### Scenario: Open file
- **WHEN** user clicks File → Open… or presses ⌘O (Ctrl+O on Windows/Linux)
- **THEN** system opens file picker dialog
#### Scenario: Save file
- **WHEN** user clicks File → Save or presses ⌘S
- **THEN** system saves current document
#### Scenario: Save As
- **WHEN** user clicks File → Save as… or presses ⌘⇧S
- **THEN** system opens save dialog for new filename
#### Scenario: Export selection
- **WHEN** user clicks File → Export selection… or presses ⌘⇧E with selection
- **THEN** system exports selected nodes as PNG
#### Scenario: Export disabled when no selection
- **WHEN** user has no selection
- **THEN** system disables "Export selection…" menu item
### Requirement: Edit menu
The system SHALL provide Edit menu with undo, redo, clipboard, and selection actions.
#### Scenario: Undo
- **WHEN** user clicks Edit → Undo or presses ⌘Z
- **THEN** system reverts last action
#### Scenario: Redo
- **WHEN** user clicks Edit → Redo or presses ⌘⇧Z
- **THEN** system reapplies undone action
#### Scenario: Copy/Paste
- **WHEN** user clicks Edit → Copy (⌘C) or Paste (⌘V)
- **THEN** system performs clipboard operation
#### Scenario: Duplicate
- **WHEN** user clicks Edit → Duplicate or presses ⌘D
- **THEN** system duplicates selected nodes
#### Scenario: Delete
- **WHEN** user clicks Edit → Delete or presses ⌫
- **THEN** system removes selected nodes
#### Scenario: Select all
- **WHEN** user clicks Edit → Select all or presses ⌘A
- **THEN** system selects all nodes on current page
### Requirement: View menu
The system SHALL provide View menu with zoom and ruler controls.
#### Scenario: Zoom to fit
- **WHEN** user clicks View → Zoom to fit or presses ⇧1
- **THEN** system fits all content in viewport
#### Scenario: Zoom in
- **WHEN** user clicks View → Zoom in or presses ⌘=
- **THEN** system zooms in toward center
#### Scenario: Zoom out
- **WHEN** user clicks View → Zoom out or presses ⌘-
- **THEN** system zooms out from center
#### Scenario: Toggle rulers
- **WHEN** user clicks View → Rulers or presses ⇧R
- **THEN** system shows/hides canvas rulers
### Requirement: Object menu
The system SHALL provide Object menu with grouping, framing, and component actions.
#### Scenario: Group selection
- **WHEN** user clicks Object → Group or presses ⌘G
- **THEN** system creates GROUP containing selected nodes
#### Scenario: Ungroup
- **WHEN** user clicks Object → Ungroup or presses ⌘⇧G with grouped selection
- **THEN** system dissolves group and reparents children
#### Scenario: Frame selection
- **WHEN** user clicks Object → Frame selection or presses ⌘⌥F
- **THEN** system wraps selected nodes in FRAME
#### Scenario: Create component
- **WHEN** user clicks Object → Create component or presses ⌘⌥K
- **THEN** system converts selection to COMPONENT
#### Scenario: Create component set
- **WHEN** user clicks Object → Create component set with multiple components selected
- **THEN** system creates COMPONENT_SET containing components
### Requirement: Text menu
The system SHALL provide Text menu with font size and style controls.
#### Scenario: Font size adjustment
- **WHEN** user clicks Text → Increase font size (⌘⇧>) or Decrease (⌘⇧<)
- **THEN** system adjusts font size of selected text by 2px
#### Scenario: Bold toggle
- **WHEN** user clicks Text → Bold or presses ⌘B
- **THEN** system toggles bold weight for selected text
#### Scenario: Italic toggle
- **WHEN** user clicks Text → Italic or presses ⌘I
- **THEN** system toggles italic style for selected text
#### Scenario: Underline toggle
- **WHEN** user clicks Text → Underline or presses ⌘U
- **THEN** system toggles underline decoration for selected text
#### Scenario: Strikethrough toggle
- **WHEN** user clicks Text → Strikethrough or presses S button
- **THEN** system toggles strikethrough decoration for selected text
#### Scenario: Text alignment submenu
- **WHEN** user clicks Text → Align → Left/Center/Right
- **THEN** system aligns selected text horizontally
### Requirement: Arrange menu
The system SHALL provide Arrange menu with z-order controls.
#### Scenario: Bring to front
- **WHEN** user clicks Arrange → Bring to front or presses ]
- **THEN** system moves selected node to top of parent's children
#### Scenario: Send to back
- **WHEN** user clicks Arrange → Send to back or presses [
- **THEN** system moves selected node to bottom of parent's children
#### Scenario: Bring forward
- **WHEN** user clicks Arrange → Bring forward
- **THEN** system moves selected node up one position
#### Scenario: Send backward
- **WHEN** user clicks Arrange → Send backward
- **THEN** system moves selected node down one position
### Requirement: Keyboard shortcut display
The system SHALL display keyboard shortcuts next to menu items.
#### Scenario: Platform-specific modifier
- **WHEN** app runs on Mac
- **THEN** system displays ⌘ for Command key
#### Scenario: Windows/Linux modifiers
- **WHEN** app runs on Windows or Linux
- **THEN** system displays Ctrl+ for Control key
### Requirement: reka-ui integration
The system SHALL use reka-ui Menubar components for menu implementation.
#### Scenario: Menu structure
- **WHEN** component renders
- **THEN** system uses MenubarRoot, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem
#### Scenario: Submenus
- **WHEN** menu has nested items (e.g., Text → Align)
- **THEN** system uses MenubarSub, MenubarSubTrigger, MenubarSubContent
#### Scenario: Separators
- **WHEN** menu defines separator: true
- **THEN** system renders MenubarSeparator divider
### Requirement: Action binding
The system SHALL bind menu actions to editor store methods.
#### Scenario: Calling store actions
- **WHEN** user selects menu item
- **THEN** system invokes corresponding method on useEditorStore (e.g., `store.saveFigFile()`, `store.duplicateSelected()`)
### Requirement: Dynamic disabled state
The system SHALL disable menu items based on current editor state.
#### Scenario: Export requires selection
- **WHEN** no nodes are selected
- **THEN** system disables "Export selection…" item
#### Scenario: Text menu requires text selection
- **WHEN** selected node is not TEXT
- **THEN** system may disable text formatting items (if implemented)

View file

@ -0,0 +1,117 @@
## ADDED Requirements
### Requirement: Automatic file saving with debounce
The system SHALL automatically save the current document 3 seconds after the last scene change.
#### Scenario: Triggering autosave
- **WHEN** user modifies scene graph (e.g., moves node, changes fill)
- **THEN** system starts 3-second timer
#### Scenario: Debouncing saves
- **WHEN** user makes multiple changes within 3 seconds
- **THEN** system resets timer on each change, saving only after 3 seconds of inactivity
#### Scenario: Writing file
- **WHEN** autosave timer expires
- **THEN** system calls `writeFile(buildFigFile())` to persist document
### Requirement: Watch sceneVersion for changes
The system SHALL monitor `state.sceneVersion` to detect modifications.
#### Scenario: Detecting changes
- **WHEN** `state.sceneVersion` increments (scene graph mutation)
- **THEN** system triggers autosave timer
#### Scenario: No save on unchanged document
- **WHEN** autosave timer expires but `sceneVersion === savedVersion`
- **THEN** system skips write (no changes since last save)
### Requirement: Disable for new unsaved files
The system SHALL not autosave documents without a file handle.
#### Scenario: New untitled document
- **WHEN** document has no associated file path or handle
- **THEN** system skips autosave (user must explicitly Save As)
#### Scenario: After Save As
- **WHEN** user performs Save As and sets file handle
- **THEN** system enables autosave for future changes
### Requirement: Use VueUse debounce
The system SHALL use `useDebounceFn` from VueUse for debouncing logic.
#### Scenario: Debounce implementation
- **WHEN** system sets up autosave watcher
- **THEN** system wraps save logic with `useDebounceFn(fn, 3000)`
### Requirement: Cross-platform file writing
The system SHALL support both Tauri and browser File System Access API.
#### Scenario: Tauri file write
- **WHEN** running in Tauri desktop app
- **THEN** system uses Tauri fs plugin to write file
#### Scenario: Browser file write
- **WHEN** running in browser with File System Access support
- **THEN** system uses FileHandle.createWritable() to write
#### Scenario: Fallback for unsupported browsers
- **WHEN** browser lacks File System Access API
- **THEN** system disables autosave (manual save only)
### Requirement: Silent failure handling
The system SHALL silently handle autosave errors without interrupting user.
#### Scenario: Write failure
- **WHEN** autosave encounters file write error (permissions, disk full)
- **THEN** system logs error silently without showing error dialog
#### Scenario: User can still manual save
- **WHEN** autosave fails
- **THEN** user can still manually trigger Save (⌘S) to retry
### Requirement: Update savedVersion after successful write
The system SHALL track last saved version to avoid redundant writes.
#### Scenario: Recording save
- **WHEN** autosave successfully writes file
- **THEN** system sets `savedVersion = state.sceneVersion`
#### Scenario: No duplicate saves
- **WHEN** autosave timer expires but versions match
- **THEN** system skips write operation
### Requirement: Configurable delay
The system SHALL use a constant `AUTOSAVE_DELAY = 3000` (3 seconds).
#### Scenario: Delay constant
- **WHEN** system sets up autosave
- **THEN** debounce delay is 3000ms (configurable via constant)
### Requirement: Cleanup on unmount
The system SHALL cancel pending autosave timers when editor unmounts.
#### Scenario: Component cleanup
- **WHEN** editor component unmounts or file closes
- **THEN** system clears timeout to prevent orphaned saves
### Requirement: Integration with editor store
The system SHALL integrate autosave into `useEditorStore` reactive state.
#### Scenario: Reactive watcher
- **WHEN** editor store initializes
- **THEN** system sets up `watch(() => state.sceneVersion, ...)` to trigger autosave
#### Scenario: Manual save updates savedVersion
- **WHEN** user manually saves (⌘S or Save As)
- **THEN** system updates `savedVersion` to prevent immediate autosave

View file

@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: Eval command for headless scripting
The CLI SHALL provide `open-pencil eval <file>` command for executing JavaScript against .fig files with a Figma-compatible `figma` global object.
#### Scenario: Inline code execution
- **WHEN** `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'` is run
- **THEN** system loads design.fig, executes code, and prints result
#### Scenario: Reading code from stdin
- **WHEN** `cat script.js | bun open-pencil eval design.fig --stdin` is run
- **THEN** system reads script from stdin and executes
#### Scenario: Writing changes back
- **WHEN** `bun open-pencil eval design.fig --code 'frame.name = "Updated"' --write` is run
- **THEN** system modifies design.fig in-place after execution
#### Scenario: Writing to output file
- **WHEN** `bun open-pencil eval design.fig --code '...' -o modified.fig` is run
- **THEN** system writes modified document to modified.fig
#### Scenario: JSON output
- **WHEN** `bun open-pencil eval design.fig --code '...' --json` is run
- **THEN** system formats result as JSON
#### Scenario: Figma API access
- **WHEN** eval code accesses `figma.createFrame()`, `figma.currentPage.findAll()`, etc.
- **THEN** system provides FigmaAPI instance bound to loaded document
#### Scenario: Error handling
- **WHEN** eval code throws error
- **THEN** system prints error message and stack trace to stderr
## MODIFIED Requirements
### Requirement: CLI commands
The CLI SHALL support the following commands:
- `open-pencil info <file>` — document stats, node type counts, font list
- `open-pencil tree <file>` — visual node tree with formatted output
- `open-pencil find <file>` — search nodes by name or type
- `open-pencil export <file>` — render to PNG/JPG/WEBP at any scale
- `open-pencil analyze colors <file>` — color palette usage with clustering
- `open-pencil analyze typography <file>` — font/size/weight distribution
- `open-pencil analyze spacing <file>` — gap/padding values with grid alignment check
- `open-pencil analyze clusters <file>` — repeated patterns (potential components)
- `open-pencil node <file> <id>` — detailed properties of a specific node
- `open-pencil pages <file>` — list pages with node counts
- `open-pencil variables <file>` — list design variables and collections
- **`open-pencil eval <file>` — execute JavaScript with Figma Plugin API (NEW)**
All commands SHALL support `--json` for machine-readable output.
#### Scenario: Info command
- **WHEN** `bun open-pencil info design.fig` is run
- **THEN** document stats, node type counts, and font list are printed
#### Scenario: Export command
- **WHEN** `bun open-pencil export design.fig --format png --scale 2` is run
- **THEN** the document is rendered headlessly and exported as 2× PNG
#### Scenario: JSON output
- **WHEN** `bun open-pencil tree design.fig --json` is run
- **THEN** the node tree is output as JSON
#### Scenario: Eval command
- **WHEN** `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'` is run
- **THEN** system executes JavaScript with `figma` global and prints result

View file

@ -0,0 +1,141 @@
## ADDED Requirements
### Requirement: Execute JavaScript against .fig files
The system SHALL provide `bun open-pencil eval <file> --code '<js>'` command for headless JavaScript execution.
#### Scenario: Running inline code
- **WHEN** user runs `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'`
- **THEN** system loads design.fig, executes code with `figma` global, prints result to stdout
#### Scenario: Returning JSON
- **WHEN** user runs `bun open-pencil eval design.fig --code 'return { id: figma.currentPage.id }'`
- **THEN** system serializes result as JSON and prints
#### Scenario: Code with no return
- **WHEN** user runs code that doesn't return a value
- **THEN** system prints `undefined`
### Requirement: Read code from stdin
The system SHALL support `--stdin` flag for reading multiline scripts.
#### Scenario: Piping script file
- **WHEN** user runs `cat script.js | bun open-pencil eval design.fig --stdin`
- **THEN** system reads script from stdin and executes
#### Scenario: Heredoc input
- **WHEN** user runs eval with heredoc `<<EOF` and multiline script
- **THEN** system reads until EOF and executes
### Requirement: Write changes back to file
The system SHALL support `--write` and `-o` flags for persisting modifications.
#### Scenario: Writing changes in-place
- **WHEN** user runs `bun open-pencil eval design.fig --code 'frame.name = "Updated"' --write`
- **THEN** system modifies design.fig in-place after execution
#### Scenario: Writing to output file
- **WHEN** user runs `bun open-pencil eval design.fig --code '...' -o modified.fig`
- **THEN** system writes modified scene graph to modified.fig, leaving original unchanged
#### Scenario: Read-only by default
- **WHEN** user runs eval without --write or -o
- **THEN** system does not modify the input file
### Requirement: Figma global object available
The system SHALL provide a `figma` global object matching Figma's Plugin API surface.
#### Scenario: Accessing figma object
- **WHEN** code accesses `figma.currentPage`, `figma.root`, `figma.createFrame()`
- **THEN** system provides FigmaAPI instance bound to loaded document
#### Scenario: Creating nodes
- **WHEN** code calls `figma.createFrame()`, `figma.createRectangle()`, etc.
- **THEN** system creates nodes in scene graph
#### Scenario: Querying nodes
- **WHEN** code calls `figma.currentPage.findAll(n => n.type === "FRAME")`
- **THEN** system traverses scene graph and returns matching nodes
### Requirement: Document loading and unloading
The system SHALL load .fig files before execution and manage cleanup.
#### Scenario: Loading document
- **WHEN** eval command starts
- **THEN** system deserializes .fig file into SceneGraph
#### Scenario: Invalid file
- **WHEN** user provides non-existent or corrupted .fig file
- **THEN** system exits with error message
### Requirement: Error handling
The system SHALL report JavaScript execution errors with stack traces.
#### Scenario: Runtime error
- **WHEN** code throws an exception (e.g., accessing property on undefined)
- **THEN** system prints error message and stack trace to stderr
#### Scenario: Syntax error
- **WHEN** code has invalid JavaScript syntax
- **THEN** system reports syntax error before execution
### Requirement: Structured output option
The system SHALL support `--json` flag for machine-readable output.
#### Scenario: JSON output
- **WHEN** user runs `bun open-pencil eval design.fig --code '...' --json`
- **THEN** system formats result as JSON (even if code doesn't return object)
#### Scenario: JSON with errors
- **WHEN** execution fails with --json flag
- **THEN** system outputs `{ "error": "message", "stack": "..." }` as JSON
### Requirement: Integration with AI tools
The system SHALL enable AI-driven design automation via eval execution.
#### Scenario: AI tool calls eval
- **WHEN** AI tool invokes eval command with generated code
- **THEN** system executes code and returns result for AI to process
#### Scenario: Batch operations
- **WHEN** AI generates loop over nodes (e.g., rename all buttons)
- **THEN** system executes batch modifications efficiently
### Requirement: Console output support
The system SHALL allow `console.log()` for debugging.
#### Scenario: Logging during execution
- **WHEN** code calls `console.log("Debug:", value)`
- **THEN** system prints to stdout before final result
### Requirement: Access to Bun APIs
The system SHALL allow code to use Bun runtime APIs.
#### Scenario: File I/O from eval code
- **WHEN** code uses `Bun.file()`, `Bun.write()`, etc.
- **THEN** system allows file operations (headless scripting context)
#### Scenario: Fetch from eval code
- **WHEN** code uses `fetch()` to call external API
- **THEN** system executes network request
### Requirement: CLI integration
The system SHALL integrate eval command into `@open-pencil/cli` package.
#### Scenario: Command registration
- **WHEN** CLI loads commands
- **THEN** eval command appears in `bun open-pencil --help`
#### Scenario: Command structure
- **WHEN** user runs `bun open-pencil eval --help`
- **THEN** system displays usage, flags (--code, --stdin, --write, -o, --json), and examples

View file

@ -0,0 +1,325 @@
## ADDED Requirements
### Requirement: Figma-compatible global object
The system SHALL provide a `figma` global object that mirrors Figma's Plugin API surface for headless JavaScript execution.
#### Scenario: Creating figma object
- **WHEN** `new FigmaAPI(sceneGraph)` is instantiated
- **THEN** the object provides methods matching Figma's plugin API (createFrame, createRectangle, createText, etc.)
#### Scenario: Accessing current page
- **WHEN** user accesses `figma.currentPage`
- **THEN** system returns a proxy for the active page with methods like `findAll`, `findOne`, `appendChild`
#### Scenario: Accessing document root
- **WHEN** user accesses `figma.root`
- **THEN** system returns a proxy for the document root containing all pages
### Requirement: Node creation methods
The system SHALL provide creation methods for all supported node types.
#### Scenario: Creating a frame
- **WHEN** user calls `figma.createFrame()`
- **THEN** system creates a FRAME node, adds it to current page, and returns a proxy
#### Scenario: Creating a rectangle
- **WHEN** user calls `figma.createRectangle()`
- **THEN** system creates a RECTANGLE node with default fill
#### Scenario: Creating text
- **WHEN** user calls `figma.createText()`
- **THEN** system creates a TEXT node with default font (Inter Regular 14px)
#### Scenario: Creating other shapes
- **WHEN** user calls `figma.createEllipse()`, `figma.createLine()`, `figma.createPolygon()`, `figma.createStar()`, `figma.createVector()`
- **THEN** system creates corresponding node types
#### Scenario: Creating component
- **WHEN** user calls `figma.createComponent()`
- **THEN** system creates a COMPONENT node
#### Scenario: Creating component set
- **WHEN** user calls `figma.createComponentSet()`
- **THEN** system creates a COMPONENT_SET node
### Requirement: Node proxy with property access
The system SHALL wrap SceneNode objects in proxies that provide Figma-compatible property getters and setters.
#### Scenario: Reading node properties
- **WHEN** user accesses `node.id`, `node.type`, `node.name`, `node.x`, `node.y`, `node.width`, `node.height`
- **THEN** system returns current values from the underlying SceneNode
#### Scenario: Setting node properties
- **WHEN** user sets `node.name = "New Name"` or `node.x = 100`
- **THEN** system updates the SceneNode via SceneGraph.updateNode()
#### Scenario: Reading removed nodes
- **WHEN** user accesses a property on a removed node
- **THEN** system throws "Node <id> has been removed"
### Requirement: Geometry and transforms
The system SHALL provide geometry properties matching Figma's API.
#### Scenario: Basic dimensions
- **WHEN** user accesses `node.width`, `node.height`, `node.rotation`
- **THEN** system returns dimensions and rotation from SceneNode
#### Scenario: Absolute position
- **WHEN** user accesses `node.absoluteTransform`
- **THEN** system returns 2x3 matrix `[[a, b, tx], [c, d, ty]]`
#### Scenario: Absolute bounding box
- **WHEN** user accesses `node.absoluteBoundingBox` or `node.absoluteRenderBounds`
- **THEN** system returns `{x, y, width, height}` in absolute coordinates
#### Scenario: Resizing nodes
- **WHEN** user calls `node.resize(200, 100)`
- **THEN** system updates node width and height
### Requirement: Fill, stroke, and effects
The system SHALL provide Figma-compatible paint and effect properties.
#### Scenario: Reading fills
- **WHEN** user accesses `node.fills`
- **THEN** system returns frozen array of Fill objects
#### Scenario: Setting fills
- **WHEN** user sets `node.fills = [{ type: "SOLID", color: { r: 1, g: 0, b: 0 } }]`
- **THEN** system updates node fills
#### Scenario: Reading strokes
- **WHEN** user accesses `node.strokes`, `node.strokeWeight`, `node.strokeAlign`
- **THEN** system returns stroke configuration
#### Scenario: Reading effects
- **WHEN** user accesses `node.effects`
- **THEN** system returns frozen array of Effect objects
### Requirement: Text node properties
The system SHALL provide text-specific properties for TEXT nodes.
#### Scenario: Reading text content
- **WHEN** user accesses `textNode.characters`
- **THEN** system returns text content string
#### Scenario: Setting text content
- **WHEN** user sets `textNode.characters = "Hello"`
- **THEN** system updates text content
#### Scenario: Font properties
- **WHEN** user accesses `textNode.fontName`, `textNode.fontSize`, `textNode.fontWeight`
- **THEN** system returns font configuration
#### Scenario: Setting font
- **WHEN** user sets `textNode.fontName = { family: "Inter", style: "Bold" }`
- **THEN** system converts style to weight (700) and updates font
#### Scenario: Text alignment
- **WHEN** user accesses `textNode.textAlignHorizontal`
- **THEN** system returns "LEFT", "CENTER", or "RIGHT"
### Requirement: Auto-layout properties
The system SHALL provide auto-layout (flexbox) properties for frames.
#### Scenario: Reading layout mode
- **WHEN** user accesses `frame.layoutMode`
- **THEN** system returns "NONE", "HORIZONTAL", or "VERTICAL"
#### Scenario: Setting layout mode
- **WHEN** user sets `frame.layoutMode = "VERTICAL"`
- **THEN** system enables auto-layout with vertical direction
#### Scenario: Spacing and padding
- **WHEN** user accesses `frame.itemSpacing`, `frame.paddingLeft`, `frame.paddingTop`, etc.
- **THEN** system returns spacing values
#### Scenario: Layout sizing
- **WHEN** user accesses `node.layoutSizingHorizontal`, `node.layoutSizingVertical`
- **THEN** system returns "FIXED", "HUG", or "FILL"
### Requirement: Tree operations
The system SHALL provide methods for manipulating the scene graph tree.
#### Scenario: Appending child
- **WHEN** user calls `parent.appendChild(child)`
- **THEN** system reparents child to parent
#### Scenario: Inserting child
- **WHEN** user calls `parent.insertChild(2, child)`
- **THEN** system inserts child at index 2 in parent's children
#### Scenario: Accessing children
- **WHEN** user accesses `parent.children`
- **THEN** system returns frozen array of child proxies
#### Scenario: Accessing parent
- **WHEN** user accesses `node.parent`
- **THEN** system returns parent proxy or null for root
#### Scenario: Removing node
- **WHEN** user calls `node.remove()`
- **THEN** system deletes node from scene graph
### Requirement: Traversal and queries
The system SHALL provide methods for finding nodes in the scene graph.
#### Scenario: Finding all matching nodes
- **WHEN** user calls `figma.currentPage.findAll(n => n.type === "FRAME")`
- **THEN** system returns array of proxies for all frames in the page
#### Scenario: Finding first matching node
- **WHEN** user calls `figma.currentPage.findOne(n => n.name === "Button")`
- **THEN** system returns first matching proxy or null
#### Scenario: Finding by ID
- **WHEN** user calls `figma.getNodeById("node-123")`
- **THEN** system returns proxy for that node or null
#### Scenario: Finding with criteria object
- **WHEN** user calls `figma.currentPage.findAllWithCriteria({ types: ["FRAME", "GROUP"] })`
- **THEN** system returns array of proxies matching type criteria
### Requirement: Selection management
The system SHALL track and expose current selection.
#### Scenario: Reading selection
- **WHEN** user accesses `figma.currentPage.selection`
- **THEN** system returns array of selected node proxies
#### Scenario: Setting selection
- **WHEN** user sets `figma.currentPage.selection = [node1, node2]`
- **THEN** system updates editor selection state
### Requirement: Component operations
The system SHALL provide component and instance methods.
#### Scenario: Creating component from node
- **WHEN** user calls `figma.createComponentFromNode(frame)`
- **THEN** system converts frame to COMPONENT and returns proxy
#### Scenario: Creating instance
- **WHEN** user calls `component.createInstance()`
- **THEN** system creates INSTANCE referencing the component
#### Scenario: Swapping instance
- **WHEN** user calls `instance.swapComponent(otherComponent)`
- **THEN** system updates instance's mainComponent reference
### Requirement: Grouping operations
The system SHALL provide grouping methods.
#### Scenario: Grouping nodes
- **WHEN** user calls `figma.group([node1, node2], parent)`
- **THEN** system creates a GROUP containing the nodes
#### Scenario: Ungrouping
- **WHEN** user calls `figma.ungroup(group)`
- **THEN** system removes group and reparents children to group's parent
### Requirement: Corner radius handling
The system SHALL handle both uniform and independent corner radii matching Figma's API.
#### Scenario: Uniform corner radius
- **WHEN** user accesses `rectangle.cornerRadius` and all corners have same radius
- **THEN** system returns that radius value
#### Scenario: Mixed corner radius
- **WHEN** user accesses `rectangle.cornerRadius` and corners have different radii
- **THEN** system returns the MIXED symbol
#### Scenario: Individual corners
- **WHEN** user accesses `rectangle.topLeftRadius`, `rectangle.topRightRadius`, etc.
- **THEN** system returns individual corner radius values
### Requirement: Cloning nodes
The system SHALL support deep cloning of nodes.
#### Scenario: Cloning a node
- **WHEN** user calls `node.clone()`
- **THEN** system creates a deep copy with new GUID and returns proxy
#### Scenario: Cloning with children
- **WHEN** user calls `frame.clone()` on a frame with children
- **THEN** system recursively clones children
### Requirement: JSON serialization
The system SHALL provide JSON export for AI tools and debugging.
#### Scenario: Exporting node to JSON
- **WHEN** user calls `figma.toJSON(node)`
- **THEN** system returns object with `type`, `name`, `id`, geometry, fills, strokes, and recursive children
#### Scenario: Exporting with depth limit
- **WHEN** user calls `figma.toJSON(node, { maxDepth: 2 })`
- **THEN** system includes children only 2 levels deep
### Requirement: Frozen arrays for safety
The system SHALL return frozen arrays for multi-value properties to prevent accidental mutation.
#### Scenario: Fills array is frozen
- **WHEN** user accesses `node.fills` and tries `fills.push(...)`
- **THEN** system throws error (array is frozen)
#### Scenario: Children array is frozen
- **WHEN** user accesses `parent.children` and tries `children[0] = other`
- **THEN** system throws error (array is frozen)
### Requirement: Internal symbols hidden
The system SHALL hide internal implementation details using Symbol properties.
#### Scenario: Internals not enumerable
- **WHEN** user calls `Object.keys(nodeProxy)` or `for (let k in nodeProxy)`
- **THEN** system does not expose INTERNAL_ID, INTERNAL_GRAPH, INTERNAL_API
### Requirement: Variable support
The system SHALL provide access to design variables.
#### Scenario: Listing variables
- **WHEN** user calls `figma.variables.getLocalVariables()`
- **THEN** system returns array of Variable objects
#### Scenario: Listing variable collections
- **WHEN** user calls `figma.variables.getLocalVariableCollections()`
- **THEN** system returns array of VariableCollection objects
#### Scenario: Getting variable by ID
- **WHEN** user calls `figma.variables.getVariableById("var-123")`
- **THEN** system returns Variable object or undefined
### Requirement: Type guards
The system SHALL provide type-checking methods matching Figma's API.
#### Scenario: Checking node type
- **WHEN** user checks `if (node.type === "FRAME")`
- **THEN** system allows type-based branching
### Requirement: Stub methods for unimplemented features
The system SHALL provide stub methods for Figma API methods not yet implemented, throwing descriptive errors.
#### Scenario: Calling unimplemented method
- **WHEN** user calls `figma.createImage(data)` or `figma.createShapeWithText()`
- **THEN** system throws "Not implemented: <method>"
#### Scenario: Notifying user
- **WHEN** user calls `figma.notify("Hello")`
- **THEN** system logs to console (headless mode has no UI notifications)

View file

@ -0,0 +1,177 @@
## ADDED Requirements
### Requirement: FigmaAPI unit tests
The project SHALL provide comprehensive unit tests for the Figma Plugin API in `tests/engine/figma-api.test.ts`.
#### Scenario: Node creation tests
- **WHEN** `bun test tests/engine/figma-api.test.ts` runs
- **THEN** tests verify `createFrame()`, `createRectangle()`, `createText()`, and other creation methods
#### Scenario: Property access tests
- **WHEN** tests read node properties (x, y, width, height, name, fills, strokes)
- **THEN** all getters return correct values from SceneNode
#### Scenario: Property setting tests
- **WHEN** tests set node properties (`node.name = "Test"`, `node.x = 100`)
- **THEN** SceneGraph is updated correctly
#### Scenario: Text property tests
- **WHEN** tests access `textNode.characters`, `fontSize`, `fontName`
- **THEN** text-specific properties work correctly
#### Scenario: Auto-layout tests
- **WHEN** tests set `layoutMode = "VERTICAL"`, `itemSpacing`, `paddingLeft`
- **THEN** auto-layout properties are applied
#### Scenario: Tree operation tests
- **WHEN** tests call `appendChild()`, `insertChild()`, `remove()`
- **THEN** scene graph tree is modified correctly
#### Scenario: Traversal tests
- **WHEN** tests call `findAll()`, `findOne()`, `findAllWithCriteria()`
- **THEN** correct nodes are returned
#### Scenario: Component tests
- **WHEN** tests create components and instances
- **THEN** component-instance relationships work
#### Scenario: Serialization tests
- **WHEN** tests call `figma.toJSON(node)`
- **THEN** JSON output includes all required properties
#### Scenario: Frozen array tests
- **WHEN** tests access `fills`, `strokes`, `children` and try to mutate
- **THEN** arrays are frozen and throw errors on mutation attempts
#### Scenario: 924 LOC of tests
- **WHEN** FigmaAPI test file is counted
- **THEN** it contains 924 lines covering 60+ test cases
### Requirement: Eval CLI integration tests
The project SHALL provide integration tests for eval command in `tests/engine/eval-cli.test.ts`.
#### Scenario: Inline code execution test
- **WHEN** test runs eval with `--code 'return figma.currentPage.id'`
- **THEN** output matches expected page ID
#### Scenario: Node creation test
- **WHEN** test runs eval creating a frame
- **THEN** document is modified and frame exists
#### Scenario: Write flag test
- **WHEN** test runs eval with `--write`
- **THEN** file is modified in-place
#### Scenario: Output file test
- **WHEN** test runs eval with `-o output.fig`
- **THEN** new file is created with modifications
#### Scenario: JSON output test
- **WHEN** test runs eval with `--json`
- **THEN** result is formatted as JSON
#### Scenario: Error handling test
- **WHEN** eval code throws error
- **THEN** stderr contains error message
#### Scenario: 202 LOC of integration tests
- **WHEN** eval CLI test file is counted
- **THEN** it contains 202 lines covering 17 integration scenarios
### Requirement: Tool schema unit tests
The project SHALL provide unit tests for unified tool definitions in `tests/engine/tools.test.ts`.
#### Scenario: Schema structure tests
- **WHEN** tests validate tool schemas
- **THEN** each tool has name, description, parameters, and handler
#### Scenario: Tool handler tests
- **WHEN** tests invoke tool handlers with valid params
- **THEN** handlers return expected results
#### Scenario: Parameter validation tests
- **WHEN** tests call handlers with invalid params
- **THEN** errors are raised or handled gracefully
#### Scenario: 390 LOC of tool tests
- **WHEN** tools test file is counted
- **THEN** it contains 390 lines
### Requirement: Tool adapter tests
The project SHALL provide adapter tests in `tests/engine/tools-ai-adapter.test.ts` (190 LOC) and `tests/engine/tools-cli.test.ts` (219 LOC).
#### Scenario: AI adapter format test
- **WHEN** AI adapter converts schema to LLM format
- **THEN** output matches expected structure
#### Scenario: CLI adapter test
- **WHEN** CLI commands use tool schema
- **THEN** parameters map correctly
### Requirement: App menu integration tests
The project SHALL provide integration tests for app menu in `tests/e2e/app-menu.spec.ts`.
#### Scenario: File menu test
- **WHEN** test clicks File menu items
- **THEN** actions (Open, Save, Export) are triggered
#### Scenario: Edit menu test
- **WHEN** test uses Edit menu (Undo, Redo, Copy, Paste)
- **THEN** operations execute correctly
#### Scenario: View menu test
- **WHEN** test uses View → Zoom in/out
- **THEN** viewport zoom changes
#### Scenario: Object menu test
- **WHEN** test uses Object → Group/Ungroup
- **THEN** nodes are grouped/ungrouped
#### Scenario: Text menu test
- **WHEN** test uses Text → Bold/Italic
- **THEN** text styles are applied
#### Scenario: Arrange menu test
- **WHEN** test uses Arrange → Bring to front
- **THEN** z-order changes
#### Scenario: 131 LOC of app menu tests
- **WHEN** app menu test file is counted
- **THEN** it contains 131 lines
### Requirement: Autosave integration tests
The project SHALL provide integration tests for autosave in `tests/e2e/autosave.spec.ts`.
#### Scenario: Autosave trigger test
- **WHEN** test modifies scene graph
- **THEN** autosave timer starts
#### Scenario: Debounce test
- **WHEN** test makes rapid changes
- **THEN** only one save occurs after 3 seconds
#### Scenario: Write test
- **WHEN** autosave timer expires
- **THEN** file is written
#### Scenario: Skip unchanged test
- **WHEN** sceneVersion matches savedVersion
- **THEN** write is skipped
#### Scenario: 113 LOC of autosave tests
- **WHEN** autosave test file is counted
- **THEN** it contains 113 lines
### Requirement: Test coverage expansion
The test suite SHALL expand from original coverage to include 2571 LOC of new tests across 7 files (figma-api.test.ts, eval-cli.test.ts, tools.test.ts, tools-ai-adapter.test.ts, tools-cli.test.ts, app-menu.spec.ts, autosave.spec.ts).
#### Scenario: Total new test lines
- **WHEN** new test files are counted
- **THEN** 2571 lines of tests are added

View file

@ -0,0 +1,89 @@
## ADDED Requirements
### Requirement: Unified tool definitions
The project SHALL define design tools once in `packages/core/src/tools/` and adapt them for AI, CLI, and MCP contexts.
#### Scenario: Canonical tool schema
- **WHEN** a new tool is added
- **THEN** it is defined in `packages/core/src/tools/schema.ts` as the single source of truth
#### Scenario: AI adapter
- **WHEN** AI assistant needs tool definitions
- **THEN** `packages/core/src/tools/ai-adapter.ts` converts schema to LLM-compatible format
#### Scenario: CLI adapter
- **WHEN** CLI commands use tools
- **THEN** citty commands consume tool schema directly
#### Scenario: MCP adapter (future)
- **WHEN** MCP server integration is added
- **THEN** MCP protocol adapter converts tool schema to MCP format
### Requirement: Tool schema structure
Tool schemas SHALL be defined in `packages/core/src/tools/schema.ts` with name, description, parameters (with types and descriptions), and handler function.
#### Scenario: Tool definition format
- **WHEN** defining a tool in schema.ts
- **THEN** structure includes `{ name, description, parameters: { <param>: { type, description } }, handler: (params) => result }`
#### Scenario: Type-safe parameters
- **WHEN** tool is invoked
- **THEN** parameters are validated against schema types
### Requirement: Deduplication of AI tools
The project SHALL eliminate duplication in `src/ai/tools.ts` by using `FigmaAPI.toJSON()` for node serialization and shared color parsing from `packages/core/src`.
#### Scenario: Node serialization
- **WHEN** AI tool returns node data
- **THEN** it uses `figmaAPI.toJSON(node)` instead of custom JSON builders
#### Scenario: Color parsing
- **WHEN** AI tool parses color input
- **THEN** it uses `parseColor()` from core instead of inline regex
#### Scenario: Code reduction
- **WHEN** AI tools are refactored
- **THEN** 311 lines are removed from `src/ai/tools.ts` via deduplication
### Requirement: Shared tool testing
The project SHALL provide test suites for tools in `tests/engine/tools.test.ts`, `tests/engine/tools-ai-adapter.test.ts`, and `tests/engine/tools-cli.test.ts`.
#### Scenario: Tool schema tests
- **WHEN** `tests/engine/tools.test.ts` runs
- **THEN** each tool schema is validated for structure and handler execution
#### Scenario: AI adapter tests
- **WHEN** `tests/engine/tools-ai-adapter.test.ts` runs
- **THEN** AI tool format conversion is verified
#### Scenario: CLI adapter tests
- **WHEN** `tests/engine/tools-cli.test.ts` runs
- **THEN** CLI command integration with tool schema is verified
### Requirement: Tool handler execution
Tool handlers SHALL receive parameters as plain objects and return structured results (success/error, data).
#### Scenario: Successful tool execution
- **WHEN** tool handler is invoked with valid params
- **THEN** it returns `{ success: true, data: <result> }`
#### Scenario: Tool execution error
- **WHEN** tool handler encounters error
- **THEN** it returns `{ success: false, error: "message" }`
### Requirement: Tool documentation
Each tool in schema.ts SHALL have clear description and parameter documentation for AI/human comprehension.
#### Scenario: Tool description
- **WHEN** AI queries available tools
- **THEN** description explains what the tool does (e.g., "Create a rectangle with specified dimensions and position")
#### Scenario: Parameter descriptions
- **WHEN** AI reads parameter schema
- **THEN** each parameter has type and description (e.g., `width: { type: 'number', description: 'Rectangle width in pixels' }`)

View file

@ -0,0 +1,93 @@
## ADDED Requirements
### Requirement: Eval command documentation
The docs site SHALL include comprehensive documentation for the eval command in `docs/eval-command.md`.
#### Scenario: Eval command page exists
- **WHEN** user navigates to `/eval-command`
- **THEN** full documentation for `open-pencil eval` command is displayed
#### Scenario: Overview section
- **WHEN** user reads eval command docs
- **THEN** overview explains purpose (headless scripting with Figma Plugin API)
#### Scenario: Usage examples
- **WHEN** user reads eval command docs
- **THEN** code examples show `--code`, `--stdin`, `--write`, `-o`, `--json` usage
#### Scenario: Architecture section
- **WHEN** user reads eval command docs
- **THEN** architecture diagram shows CLI → loadDocument → FigmaAPI → execute → serialize flow
#### Scenario: FigmaAPI surface coverage
- **WHEN** user reads eval command docs
- **THEN** documentation lists supported methods (createFrame, createRectangle, findAll, etc.)
#### Scenario: AI integration examples
- **WHEN** user reads eval command docs
- **THEN** examples show how AI tools use eval for batch operations
#### Scenario: Testing patterns
- **WHEN** user reads eval command docs
- **THEN** examples show headless testing with eval
#### Scenario: Migration from Figma plugins
- **WHEN** user reads eval command docs
- **THEN** guide explains how to adapt Figma plugin code to eval scripts
#### Scenario: 437 lines of documentation
- **WHEN** eval-command.md is counted
- **THEN** it contains 437 lines of comprehensive content
### Requirement: Comparison matrix updates
The docs site SHALL update comparison matrices to reflect new features (app menu, eval command, Figma Plugin API).
#### Scenario: Figma comparison update
- **WHEN** user reads `docs/guide/figma-comparison.md`
- **THEN** Interface & Navigation section includes app menu status
#### Scenario: Plugin API row
- **WHEN** user reads Figma comparison
- **THEN** a row documents eval command with Figma Plugin API compatibility
#### Scenario: AI tools update
- **WHEN** user reads Figma comparison
- **THEN** AI tools row is updated to reflect unified tool definitions and eval integration
#### Scenario: Penpot comparison update
- **WHEN** user reads `docs/guide/comparison.md`
- **THEN** Architecture section highlights headless scripting advantage (eval command with Plugin API that Penpot lacks)
### Requirement: App menu documentation
The docs site SHALL document app menu in appropriate guide pages.
#### Scenario: App menu in features
- **WHEN** user reads Features page
- **THEN** app menu is listed with menus (File, Edit, View, Object, Text, Arrange) and key actions
#### Scenario: Browser mode distinction
- **WHEN** user reads app menu docs
- **THEN** clarification that menu bar only appears in browser mode (Tauri uses native menus)
### Requirement: Autosave documentation
The docs site SHALL document autosave behavior in appropriate guide pages.
#### Scenario: Autosave in features
- **WHEN** user reads Features page or Getting Started
- **THEN** autosave is explained (3-second debounce, automatic for files with handle)
#### Scenario: Autosave limitations
- **WHEN** user reads autosave docs
- **THEN** note that new unsaved files don't autosave until user performs Save As
### Requirement: AGENTS.md update reference
The docs site SHALL reference tool unification in development pages when relevant.
#### Scenario: Tool architecture mention
- **WHEN** user reads development/contributing docs
- **THEN** unified tool definitions (`packages/core/src/tools/`) are mentioned as canonical source

View file

@ -0,0 +1,83 @@
## Context
This change syncs OpenSpec specs and VitePress documentation with master branch commits 63748ec through 20290f6. All code is already implemented and tested (2571 LOC of new tests). Tasks focus on documentation sync, not code changes.
**References:**
- Proposal: `openspec/changes/sync-specs-docs-with-master/proposal.md` — why and what
- Design: `openspec/changes/sync-specs-docs-with-master/design.md` — how to sync (code as source of truth)
- Delta specs: `openspec/changes/sync-specs-docs-with-master/specs/` — requirements derived from implemented code
**Note:** `docs/eval-command.md` already exists (437 lines, merged from master). Tasks update cross-references and navigation, not content creation.
## 1. Create new capability specs
- [x] 1.1 Create `openspec/specs/figma-plugin-api/spec.md` by resolving delta spec (strip ADDED markers, keep requirement content verbatim)
- [x] 1.2 Create `openspec/specs/eval-command/spec.md` by resolving delta spec
- [x] 1.3 Create `openspec/specs/app-menu/spec.md` by resolving delta spec
- [x] 1.4 Create `openspec/specs/autosave/spec.md` by resolving delta spec
## 2. Update existing capability specs with delta changes
**Merge strategy:**
- **ADDED Requirements** → append to end of requirements section
- **MODIFIED Requirements** → find matching requirement by header text (whitespace-insensitive), replace entire block (from `### Requirement:` through all scenarios). If no exact match, show diff and resolve manually.
- **REMOVED Requirements** → delete requirement block, add deprecation note if needed
- Preserve existing spec structure (Purpose, other requirements)
- [x] 2.1 Merge cli delta: read `changes/.../specs/cli/spec.md`, append ADDED requirements, replace MODIFIED "CLI commands" requirement in `openspec/specs/cli/spec.md`
- [x] 2.2 Merge tooling delta: append ADDED requirements from `changes/.../specs/tooling/spec.md` to `openspec/specs/tooling/spec.md`
- [x] 2.3 Merge testing delta: append ADDED requirements from `changes/.../specs/testing/spec.md` to `openspec/specs/testing/spec.md`
- [x] 2.4 Merge vitepress-docs delta: append ADDED requirements from `changes/.../specs/vitepress-docs/spec.md` to `openspec/specs/vitepress-docs/spec.md`
## 3. Update Figma comparison matrix
**Table format:** Markdown table with columns: Feature | Status | Notes. Status uses emoji: ✅ Supported, 🟡 Partial, 🔲 Not yet.
- [x] 3.1 Add row to Interface & Navigation table: `| App menu (browser mode) | ✅ | File, Edit, View, Object, Text, Arrange menus; Tauri uses native menus |`
- [x] 3.2 Update AI tools row in Interface & Navigation: change status to 🟡, update Notes to mention "10 tools via OpenRouter + unified tool definitions + eval command integration; no AI image generation yet"
- [x] 3.3 Add new "Plugin API & Scripting" section after "Import & Export" with table row: `| Eval command with Figma Plugin API | ✅ | Headless JavaScript execution with figma global object matching Figma's plugin surface |`
- [x] 3.4 Recalculate coverage stats: 85 of 152 addressed (66 ✅, 19 🟡, 67 🔲)
## 4. Update Penpot comparison
- [x] 4.1 Add paragraph to Architecture section in `docs/guide/comparison.md` after "Verdict: Architecture" subsection: "OpenPencil's eval command with Figma Plugin API enables headless scripting and automation that Penpot lacks. Penpot has no plugin system; OpenPencil's `figma` global object mirrors Figma's API for script portability." Link to `/eval-command` for details. (Note: eval-command.md already has 437 lines of examples; link instead of duplicating)
## 5. Update Features documentation
- [x] 5.1 Add app menu entry to `docs/guide/features.md` (File, Edit, View, Object, Text, Arrange menus; browser-only note)
- [x] 5.2 Add autosave entry to `docs/guide/features.md` (3-second debounce, file handle requirement)
- [x] 5.3 Add headless scripting / eval command entry to `docs/guide/features.md` (link to eval-command.md)
- [x] 5.4 Update AI tools entry in `docs/guide/features.md` to mention unified tool definitions
- [x] 5.5 Landing page feature cards already cover eval/CLI via "Programmable" card — no changes needed
## 6. Update VitePress navigation
**Note:** New spec files (figma-plugin-api, eval-command, app-menu, autosave) are OpenSpec internal specs, not user-facing docs. Only eval-command has user docs at `docs/eval-command.md`.
- [x] 6.1 Add "Eval Command" entry to Reference sidebar in `docs/.vitepress/config.ts`: inserted after "File Format" in Reference items array
- [x] 6.2 Verify deferred to section 8
## 7. Update AGENTS.md
**Note:** AGENTS.md already correctly documents tools (`packages/core/src/tools/schema.ts`), FigmaAPI (`packages/core/src/figma-api.ts`), and eval command in "Tools (AI / MCP / CLI)" section per design.md. Only UI section needs update.
- [x] 7.1 Added AppMenu bullet to UI section in AGENTS.md
## 8. Verification
- [x] 8.1 `bun run docs:build` passed — no broken links, built in 35.95s
- [x] 8.2 eval-command.html generated in dist/
- [x] 8.2a Cross-links verified via successful build (VitePress validates internal links)
- [x] 8.3 Figma comparison: app menu row added, AI tools updated, Plugin API section added, stats recalculated (85/152)
- [x] 8.4 Penpot comparison: headless scripting paragraph added with /eval-command link
- [x] 8.5 All 4 new spec files have valid heading hierarchy (### Requirement + #### Scenario)
- [x] 8.6 All 4 merged specs: additions only (0 removed lines), existing requirements preserved
- [x] 8.7 AGENTS.md: AppMenu bullet added in correct format within UI section
- [x] 8.8 Sidebar config updated with Eval Command entry in Reference group
## 9. Archive preparation
- [x] 9.1 All tasks complete
- [x] 9.2 All artifacts present: proposal.md, design.md, specs/ (8 delta specs), tasks.md
- [x] 9.3 Delta specs merged: 4 new specs created, 4 existing specs updated (additions only)
- [ ] 9.4 Ready for `/opsx:archive`

View file

@ -0,0 +1,210 @@
# app-menu Specification
## Purpose
Browser-mode menu bar (`src/components/AppMenu.vue`) with File, Edit, View, Object, Text, and Arrange menus. Uses reka-ui Menubar components. Only visible when `!IS_TAURI` (Tauri provides native menus).
## Requirements
### Requirement: App menu bar for browser mode
The system SHALL display a menu bar in browser mode (`!IS_TAURI`) with File, Edit, View, Object, Text, and Arrange menus.
#### Scenario: Menu visibility in browser
- **WHEN** app runs in browser (not Tauri desktop)
- **THEN** system displays menu bar at top of window
#### Scenario: Menu hidden in Tauri
- **WHEN** app runs in Tauri desktop mode
- **THEN** system hides menu bar (Tauri provides native menus)
### Requirement: File menu
The system SHALL provide File menu with Open, Save, Save As, and Export actions.
#### Scenario: Open file
- **WHEN** user clicks File → Open… or presses ⌘O (Ctrl+O on Windows/Linux)
- **THEN** system opens file picker dialog
#### Scenario: Save file
- **WHEN** user clicks File → Save or presses ⌘S
- **THEN** system saves current document
#### Scenario: Save As
- **WHEN** user clicks File → Save as… or presses ⌘⇧S
- **THEN** system opens save dialog for new filename
#### Scenario: Export selection
- **WHEN** user clicks File → Export selection… or presses ⌘⇧E with selection
- **THEN** system exports selected nodes as PNG
#### Scenario: Export disabled when no selection
- **WHEN** user has no selection
- **THEN** system disables "Export selection…" menu item
### Requirement: Edit menu
The system SHALL provide Edit menu with undo, redo, clipboard, and selection actions.
#### Scenario: Undo
- **WHEN** user clicks Edit → Undo or presses ⌘Z
- **THEN** system reverts last action
#### Scenario: Redo
- **WHEN** user clicks Edit → Redo or presses ⌘⇧Z
- **THEN** system reapplies undone action
#### Scenario: Copy/Paste
- **WHEN** user clicks Edit → Copy (⌘C) or Paste (⌘V)
- **THEN** system performs clipboard operation
#### Scenario: Duplicate
- **WHEN** user clicks Edit → Duplicate or presses ⌘D
- **THEN** system duplicates selected nodes
#### Scenario: Delete
- **WHEN** user clicks Edit → Delete or presses ⌫
- **THEN** system removes selected nodes
#### Scenario: Select all
- **WHEN** user clicks Edit → Select all or presses ⌘A
- **THEN** system selects all nodes on current page
### Requirement: View menu
The system SHALL provide View menu with zoom and ruler controls.
#### Scenario: Zoom to fit
- **WHEN** user clicks View → Zoom to fit or presses ⇧1
- **THEN** system fits all content in viewport
#### Scenario: Zoom in
- **WHEN** user clicks View → Zoom in or presses ⌘=
- **THEN** system zooms in toward center
#### Scenario: Zoom out
- **WHEN** user clicks View → Zoom out or presses ⌘-
- **THEN** system zooms out from center
#### Scenario: Toggle rulers
- **WHEN** user clicks View → Rulers or presses ⇧R
- **THEN** system shows/hides canvas rulers
### Requirement: Object menu
The system SHALL provide Object menu with grouping, framing, and component actions.
#### Scenario: Group selection
- **WHEN** user clicks Object → Group or presses ⌘G
- **THEN** system creates GROUP containing selected nodes
#### Scenario: Ungroup
- **WHEN** user clicks Object → Ungroup or presses ⌘⇧G with grouped selection
- **THEN** system dissolves group and reparents children
#### Scenario: Frame selection
- **WHEN** user clicks Object → Frame selection or presses ⌘⌥F
- **THEN** system wraps selected nodes in FRAME
#### Scenario: Create component
- **WHEN** user clicks Object → Create component or presses ⌘⌥K
- **THEN** system converts selection to COMPONENT
#### Scenario: Create component set
- **WHEN** user clicks Object → Create component set with multiple components selected
- **THEN** system creates COMPONENT_SET containing components
### Requirement: Text menu
The system SHALL provide Text menu with font size and style controls.
#### Scenario: Font size adjustment
- **WHEN** user clicks Text → Increase font size (⌘⇧>) or Decrease (⌘⇧<)
- **THEN** system adjusts font size of selected text by 2px
#### Scenario: Bold toggle
- **WHEN** user clicks Text → Bold or presses ⌘B
- **THEN** system toggles bold weight for selected text
#### Scenario: Italic toggle
- **WHEN** user clicks Text → Italic or presses ⌘I
- **THEN** system toggles italic style for selected text
#### Scenario: Underline toggle
- **WHEN** user clicks Text → Underline or presses ⌘U
- **THEN** system toggles underline decoration for selected text
#### Scenario: Strikethrough toggle
- **WHEN** user clicks Text → Strikethrough or presses S button
- **THEN** system toggles strikethrough decoration for selected text
#### Scenario: Text alignment submenu
- **WHEN** user clicks Text → Align → Left/Center/Right
- **THEN** system aligns selected text horizontally
### Requirement: Arrange menu
The system SHALL provide Arrange menu with z-order controls.
#### Scenario: Bring to front
- **WHEN** user clicks Arrange → Bring to front or presses ]
- **THEN** system moves selected node to top of parent's children
#### Scenario: Send to back
- **WHEN** user clicks Arrange → Send to back or presses [
- **THEN** system moves selected node to bottom of parent's children
#### Scenario: Bring forward
- **WHEN** user clicks Arrange → Bring forward
- **THEN** system moves selected node up one position
#### Scenario: Send backward
- **WHEN** user clicks Arrange → Send backward
- **THEN** system moves selected node down one position
### Requirement: Keyboard shortcut display
The system SHALL display keyboard shortcuts next to menu items.
#### Scenario: Platform-specific modifier
- **WHEN** app runs on Mac
- **THEN** system displays ⌘ for Command key
#### Scenario: Windows/Linux modifiers
- **WHEN** app runs on Windows or Linux
- **THEN** system displays Ctrl+ for Control key
### Requirement: reka-ui integration
The system SHALL use reka-ui Menubar components for menu implementation.
#### Scenario: Menu structure
- **WHEN** component renders
- **THEN** system uses MenubarRoot, MenubarMenu, MenubarTrigger, MenubarContent, MenubarItem
#### Scenario: Submenus
- **WHEN** menu has nested items (e.g., Text → Align)
- **THEN** system uses MenubarSub, MenubarSubTrigger, MenubarSubContent
#### Scenario: Separators
- **WHEN** menu defines separator: true
- **THEN** system renders MenubarSeparator divider
### Requirement: Action binding
The system SHALL bind menu actions to editor store methods.
#### Scenario: Calling store actions
- **WHEN** user selects menu item
- **THEN** system invokes corresponding method on useEditorStore (e.g., `store.saveFigFile()`, `store.duplicateSelected()`)
### Requirement: Dynamic disabled state
The system SHALL disable menu items based on current editor state.
#### Scenario: Export requires selection
- **WHEN** no nodes are selected
- **THEN** system disables "Export selection…" item
#### Scenario: Text menu requires text selection
- **WHEN** selected node is not TEXT
- **THEN** system may disable text formatting items (if implemented)

View file

@ -0,0 +1,122 @@
# autosave Specification
## Purpose
Automatic file saving with 3-second debounce after scene changes. Integrated into `useEditorStore`. Uses `useDebounceFn` from VueUse. Supports Tauri and browser File System Access API. Disabled for new unsaved files.
## Requirements
### Requirement: Automatic file saving with debounce
The system SHALL automatically save the current document 3 seconds after the last scene change.
#### Scenario: Triggering autosave
- **WHEN** user modifies scene graph (e.g., moves node, changes fill)
- **THEN** system starts 3-second timer
#### Scenario: Debouncing saves
- **WHEN** user makes multiple changes within 3 seconds
- **THEN** system resets timer on each change, saving only after 3 seconds of inactivity
#### Scenario: Writing file
- **WHEN** autosave timer expires
- **THEN** system calls `writeFile(buildFigFile())` to persist document
### Requirement: Watch sceneVersion for changes
The system SHALL monitor `state.sceneVersion` to detect modifications.
#### Scenario: Detecting changes
- **WHEN** `state.sceneVersion` increments (scene graph mutation)
- **THEN** system triggers autosave timer
#### Scenario: No save on unchanged document
- **WHEN** autosave timer expires but `sceneVersion === savedVersion`
- **THEN** system skips write (no changes since last save)
### Requirement: Disable for new unsaved files
The system SHALL not autosave documents without a file handle.
#### Scenario: New untitled document
- **WHEN** document has no associated file path or handle
- **THEN** system skips autosave (user must explicitly Save As)
#### Scenario: After Save As
- **WHEN** user performs Save As and sets file handle
- **THEN** system enables autosave for future changes
### Requirement: Use VueUse debounce
The system SHALL use `useDebounceFn` from VueUse for debouncing logic.
#### Scenario: Debounce implementation
- **WHEN** system sets up autosave watcher
- **THEN** system wraps save logic with `useDebounceFn(fn, 3000)`
### Requirement: Cross-platform file writing
The system SHALL support both Tauri and browser File System Access API.
#### Scenario: Tauri file write
- **WHEN** running in Tauri desktop app
- **THEN** system uses Tauri fs plugin to write file
#### Scenario: Browser file write
- **WHEN** running in browser with File System Access support
- **THEN** system uses FileHandle.createWritable() to write
#### Scenario: Fallback for unsupported browsers
- **WHEN** browser lacks File System Access API
- **THEN** system disables autosave (manual save only)
### Requirement: Silent failure handling
The system SHALL silently handle autosave errors without interrupting user.
#### Scenario: Write failure
- **WHEN** autosave encounters file write error (permissions, disk full)
- **THEN** system logs error silently without showing error dialog
#### Scenario: User can still manual save
- **WHEN** autosave fails
- **THEN** user can still manually trigger Save (⌘S) to retry
### Requirement: Update savedVersion after successful write
The system SHALL track last saved version to avoid redundant writes.
#### Scenario: Recording save
- **WHEN** autosave successfully writes file
- **THEN** system sets `savedVersion = state.sceneVersion`
#### Scenario: No duplicate saves
- **WHEN** autosave timer expires but versions match
- **THEN** system skips write operation
### Requirement: Fixed delay constant
The system SHALL use a compile-time constant `AUTOSAVE_DELAY = 3000` (3 seconds).
#### Scenario: Delay constant
- **WHEN** system sets up autosave
- **THEN** debounce delay is 3000ms
### Requirement: Cleanup on unmount
The system SHALL cancel pending autosave timers when editor unmounts.
#### Scenario: Component cleanup
- **WHEN** editor component unmounts or file closes
- **THEN** system clears timeout to prevent orphaned saves
### Requirement: Integration with editor store
The system SHALL integrate autosave into `useEditorStore` reactive state.
#### Scenario: Reactive watcher
- **WHEN** editor store initializes
- **THEN** system sets up `watch(() => state.sceneVersion, ...)` to trigger autosave
#### Scenario: Manual save updates savedVersion
- **WHEN** user manually saves (⌘S or Save As)
- **THEN** system updates `savedVersion` to prevent immediate autosave

View file

@ -21,6 +21,7 @@ The CLI SHALL support the following commands:
- `open-pencil node <file> <id>` — detailed properties of a specific node
- `open-pencil pages <file>` — list pages with node counts
- `open-pencil variables <file>` — list design variables and collections
- `open-pencil eval <file>` — execute JavaScript with Figma Plugin API
All commands SHALL support `--json` for machine-readable output.
@ -36,6 +37,10 @@ All commands SHALL support `--json` for machine-readable output.
- **WHEN** `bun open-pencil tree design.fig --json` is run
- **THEN** the node tree is output as JSON
#### Scenario: Eval command
- **WHEN** `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'` is run
- **THEN** system executes JavaScript with `figma` global and prints result
### Requirement: Workspace integration
The CLI SHALL be runnable via `bun open-pencil` within the Bun workspace, without global installation.
@ -74,3 +79,35 @@ The CLI SHALL provide `open-pencil variables <file>` to list design variables an
#### Scenario: List variables
- **WHEN** `bun open-pencil variables design.fig` is run
- **THEN** all variable collections, modes, and variable values are listed
### Requirement: Eval command for headless scripting
The CLI SHALL provide `open-pencil eval <file>` command for executing JavaScript against .fig files with a Figma-compatible `figma` global object.
#### Scenario: Inline code execution
- **WHEN** `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'` is run
- **THEN** system loads design.fig, executes code, and prints result
#### Scenario: Reading code from stdin
- **WHEN** `cat script.js | bun open-pencil eval design.fig --stdin` is run
- **THEN** system reads script from stdin and executes
#### Scenario: Writing changes back
- **WHEN** `bun open-pencil eval design.fig --code 'frame.name = "Updated"' --write` is run
- **THEN** system modifies design.fig in-place after execution
#### Scenario: Writing to output file
- **WHEN** `bun open-pencil eval design.fig --code '...' -o modified.fig` is run
- **THEN** system writes modified document to modified.fig
#### Scenario: JSON output
- **WHEN** `bun open-pencil eval design.fig --code '...' --json` is run
- **THEN** system formats result as JSON
#### Scenario: Figma API access
- **WHEN** eval code accesses `figma.createFrame()`, `figma.currentPage.findAll()`, etc.
- **THEN** system provides FigmaAPI instance bound to loaded document
#### Scenario: Error handling
- **WHEN** eval code throws error
- **THEN** system prints error message and stack trace to stderr

View file

@ -0,0 +1,146 @@
# eval-command Specification
## Purpose
CLI command `open-pencil eval <file>` for executing JavaScript against .fig files with a Figma-compatible `figma` global object. Enables headless scripting, batch operations, AI tool execution, and testing without GUI. Supports `--code`, `--stdin`, `--write`, `-o`, and `--json` flags.
## Requirements
### Requirement: Execute JavaScript against .fig files
The system SHALL provide `bun open-pencil eval <file> --code '<js>'` command for headless JavaScript execution.
#### Scenario: Running inline code
- **WHEN** user runs `bun open-pencil eval design.fig --code 'return figma.currentPage.children.length'`
- **THEN** system loads design.fig, executes code with `figma` global, prints result to stdout
#### Scenario: Returning JSON
- **WHEN** user runs `bun open-pencil eval design.fig --code 'return { id: figma.currentPage.id }'`
- **THEN** system serializes result as JSON and prints
#### Scenario: Code with no return
- **WHEN** user runs code that doesn't return a value
- **THEN** system prints `undefined`
### Requirement: Read code from stdin
The system SHALL support `--stdin` flag for reading multiline scripts.
#### Scenario: Piping script file
- **WHEN** user runs `cat script.js | bun open-pencil eval design.fig --stdin`
- **THEN** system reads script from stdin and executes
#### Scenario: Heredoc input
- **WHEN** user runs eval with heredoc `<<EOF` and multiline script
- **THEN** system reads until EOF and executes
### Requirement: Write changes back to file
The system SHALL support `--write` and `-o` flags for persisting modifications.
#### Scenario: Writing changes in-place
- **WHEN** user runs `bun open-pencil eval design.fig --code 'frame.name = "Updated"' --write`
- **THEN** system modifies design.fig in-place after execution
#### Scenario: Writing to output file
- **WHEN** user runs `bun open-pencil eval design.fig --code '...' -o modified.fig`
- **THEN** system writes modified scene graph to modified.fig, leaving original unchanged
#### Scenario: Read-only by default
- **WHEN** user runs eval without --write or -o
- **THEN** system does not modify the input file
### Requirement: Figma global object available
The system SHALL provide a `figma` global object matching Figma's Plugin API surface.
#### Scenario: Accessing figma object
- **WHEN** code accesses `figma.currentPage`, `figma.root`, `figma.createFrame()`
- **THEN** system provides FigmaAPI instance bound to loaded document
#### Scenario: Creating nodes
- **WHEN** code calls `figma.createFrame()`, `figma.createRectangle()`, etc.
- **THEN** system creates nodes in scene graph
#### Scenario: Querying nodes
- **WHEN** code calls `figma.currentPage.findAll(n => n.type === "FRAME")`
- **THEN** system traverses scene graph and returns matching nodes
### Requirement: Document loading and unloading
The system SHALL load .fig files before execution and manage cleanup.
#### Scenario: Loading document
- **WHEN** eval command starts
- **THEN** system deserializes .fig file into SceneGraph
#### Scenario: Invalid file
- **WHEN** user provides non-existent or corrupted .fig file
- **THEN** system exits with error message
### Requirement: Error handling
The system SHALL report JavaScript execution errors with stack traces.
#### Scenario: Runtime error
- **WHEN** code throws an exception (e.g., accessing property on undefined)
- **THEN** system prints error message and stack trace to stderr
#### Scenario: Syntax error
- **WHEN** code has invalid JavaScript syntax
- **THEN** system reports syntax error before execution
### Requirement: Structured output option
The system SHALL support `--json` flag for machine-readable output.
#### Scenario: JSON output
- **WHEN** user runs `bun open-pencil eval design.fig --code '...' --json`
- **THEN** system formats result as JSON (even if code doesn't return object)
#### Scenario: JSON with errors
- **WHEN** execution fails with --json flag
- **THEN** system outputs `{ "error": "message", "stack": "..." }` as JSON
### Requirement: Integration with AI tools
The system SHALL enable AI-driven design automation via eval execution.
#### Scenario: AI tool calls eval
- **WHEN** AI tool invokes eval command with generated code
- **THEN** system executes code and returns result for AI to process
#### Scenario: Batch operations
- **WHEN** AI generates loop over nodes (e.g., rename all buttons)
- **THEN** system executes batch modifications efficiently
### Requirement: Console output support
The system SHALL allow `console.log()` for debugging.
#### Scenario: Logging during execution
- **WHEN** code calls `console.log("Debug:", value)`
- **THEN** system prints to stdout before final result
### Requirement: Access to Bun APIs
The system SHALL allow code to use Bun runtime APIs. Eval runs in a trusted local context (same as the CLI itself) — no sandboxing is applied.
#### Scenario: File I/O from eval code
- **WHEN** code uses `Bun.file()`, `Bun.write()`, etc.
- **THEN** system allows file operations (trusted headless context)
#### Scenario: Fetch from eval code
- **WHEN** code uses `fetch()` to call external API
- **THEN** system executes network request
### Requirement: CLI integration
The system SHALL integrate eval command into `@open-pencil/cli` package.
#### Scenario: Command registration
- **WHEN** CLI loads commands
- **THEN** eval command appears in `bun open-pencil --help`
#### Scenario: Command structure
- **WHEN** user runs `bun open-pencil eval --help`
- **THEN** system displays usage, flags (--code, --stdin, --write, -o, --json), and examples

View file

@ -0,0 +1,330 @@
# figma-plugin-api Specification
## Purpose
Figma-compatible Plugin API for headless JavaScript execution. Provides `figma` global object (FigmaAPI class) that mirrors Figma's plugin surface for scripting .fig files. Enables eval command, AI tools, and automated batch operations without GUI.
## Requirements
### Requirement: Figma-compatible global object
The system SHALL provide a `figma` global object that mirrors Figma's Plugin API surface for headless JavaScript execution.
#### Scenario: Creating figma object
- **WHEN** `new FigmaAPI(sceneGraph)` is instantiated
- **THEN** the object provides methods matching Figma's plugin API (createFrame, createRectangle, createText, etc.)
#### Scenario: Accessing current page
- **WHEN** user accesses `figma.currentPage`
- **THEN** system returns a proxy for the active page with methods like `findAll`, `findOne`, `appendChild`
#### Scenario: Accessing document root
- **WHEN** user accesses `figma.root`
- **THEN** system returns a proxy for the document root containing all pages
### Requirement: Node creation methods
The system SHALL provide creation methods for all supported node types.
#### Scenario: Creating a frame
- **WHEN** user calls `figma.createFrame()`
- **THEN** system creates a FRAME node, adds it to current page, and returns a proxy
#### Scenario: Creating a rectangle
- **WHEN** user calls `figma.createRectangle()`
- **THEN** system creates a RECTANGLE node with default fill
#### Scenario: Creating text
- **WHEN** user calls `figma.createText()`
- **THEN** system creates a TEXT node with default font (Inter Regular 14px)
#### Scenario: Creating other shapes
- **WHEN** user calls `figma.createEllipse()`, `figma.createLine()`, `figma.createPolygon()`, `figma.createStar()`, `figma.createVector()`
- **THEN** system creates corresponding node types
#### Scenario: Creating component
- **WHEN** user calls `figma.createComponent()`
- **THEN** system creates a COMPONENT node
#### Scenario: Creating component set
- **WHEN** user calls `figma.createComponentSet()`
- **THEN** system creates a COMPONENT_SET node
### Requirement: Node proxy with property access
The system SHALL wrap SceneNode objects in proxies that provide Figma-compatible property getters and setters.
#### Scenario: Reading node properties
- **WHEN** user accesses `node.id`, `node.type`, `node.name`, `node.x`, `node.y`, `node.width`, `node.height`
- **THEN** system returns current values from the underlying SceneNode
#### Scenario: Setting node properties
- **WHEN** user sets `node.name = "New Name"` or `node.x = 100`
- **THEN** system updates the SceneNode via SceneGraph.updateNode()
#### Scenario: Reading removed nodes
- **WHEN** user accesses a property on a removed node
- **THEN** system throws "Node <id> has been removed"
### Requirement: Geometry and transforms
The system SHALL provide geometry properties matching Figma's API.
#### Scenario: Basic dimensions
- **WHEN** user accesses `node.width`, `node.height`, `node.rotation`
- **THEN** system returns dimensions and rotation from SceneNode
#### Scenario: Absolute position
- **WHEN** user accesses `node.absoluteTransform`
- **THEN** system returns 2x3 matrix `[[a, b, tx], [c, d, ty]]`
#### Scenario: Absolute bounding box
- **WHEN** user accesses `node.absoluteBoundingBox` or `node.absoluteRenderBounds`
- **THEN** system returns `{x, y, width, height}` in absolute coordinates
#### Scenario: Resizing nodes
- **WHEN** user calls `node.resize(200, 100)`
- **THEN** system updates node width and height
### Requirement: Fill, stroke, and effects
The system SHALL provide Figma-compatible paint and effect properties.
#### Scenario: Reading fills
- **WHEN** user accesses `node.fills`
- **THEN** system returns frozen array of Fill objects
#### Scenario: Setting fills
- **WHEN** user sets `node.fills = [{ type: "SOLID", color: { r: 1, g: 0, b: 0 } }]`
- **THEN** system updates node fills
#### Scenario: Reading strokes
- **WHEN** user accesses `node.strokes`, `node.strokeWeight`, `node.strokeAlign`
- **THEN** system returns stroke configuration
#### Scenario: Reading effects
- **WHEN** user accesses `node.effects`
- **THEN** system returns frozen array of Effect objects
### Requirement: Text node properties
The system SHALL provide text-specific properties for TEXT nodes.
#### Scenario: Reading text content
- **WHEN** user accesses `textNode.characters`
- **THEN** system returns text content string
#### Scenario: Setting text content
- **WHEN** user sets `textNode.characters = "Hello"`
- **THEN** system updates text content
#### Scenario: Font properties
- **WHEN** user accesses `textNode.fontName`, `textNode.fontSize`, `textNode.fontWeight`
- **THEN** system returns font configuration
#### Scenario: Setting font
- **WHEN** user sets `textNode.fontName = { family: "Inter", style: "Bold" }`
- **THEN** system converts style to weight (700) and updates font
#### Scenario: Text alignment
- **WHEN** user accesses `textNode.textAlignHorizontal`
- **THEN** system returns "LEFT", "CENTER", or "RIGHT"
### Requirement: Auto-layout properties
The system SHALL provide auto-layout (flexbox) properties for frames.
#### Scenario: Reading layout mode
- **WHEN** user accesses `frame.layoutMode`
- **THEN** system returns "NONE", "HORIZONTAL", or "VERTICAL"
#### Scenario: Setting layout mode
- **WHEN** user sets `frame.layoutMode = "VERTICAL"`
- **THEN** system enables auto-layout with vertical direction
#### Scenario: Spacing and padding
- **WHEN** user accesses `frame.itemSpacing`, `frame.paddingLeft`, `frame.paddingTop`, etc.
- **THEN** system returns spacing values
#### Scenario: Layout sizing
- **WHEN** user accesses `node.layoutSizingHorizontal`, `node.layoutSizingVertical`
- **THEN** system returns "FIXED", "HUG", or "FILL"
### Requirement: Tree operations
The system SHALL provide methods for manipulating the scene graph tree.
#### Scenario: Appending child
- **WHEN** user calls `parent.appendChild(child)`
- **THEN** system reparents child to parent
#### Scenario: Inserting child
- **WHEN** user calls `parent.insertChild(2, child)`
- **THEN** system inserts child at index 2 in parent's children
#### Scenario: Accessing children
- **WHEN** user accesses `parent.children`
- **THEN** system returns frozen array of child proxies
#### Scenario: Accessing parent
- **WHEN** user accesses `node.parent`
- **THEN** system returns parent proxy or null for root
#### Scenario: Removing node
- **WHEN** user calls `node.remove()`
- **THEN** system deletes node from scene graph
### Requirement: Traversal and queries
The system SHALL provide methods for finding nodes in the scene graph.
#### Scenario: Finding all matching nodes
- **WHEN** user calls `figma.currentPage.findAll(n => n.type === "FRAME")`
- **THEN** system returns array of proxies for all frames in the page
#### Scenario: Finding first matching node
- **WHEN** user calls `figma.currentPage.findOne(n => n.name === "Button")`
- **THEN** system returns first matching proxy or null
#### Scenario: Finding by ID
- **WHEN** user calls `figma.getNodeById("node-123")`
- **THEN** system returns proxy for that node or null
#### Scenario: Finding with criteria object
- **WHEN** user calls `figma.currentPage.findAllWithCriteria({ types: ["FRAME", "GROUP"] })`
- **THEN** system returns array of proxies matching type criteria
### Requirement: Selection management
The system SHALL track and expose current selection.
#### Scenario: Reading selection
- **WHEN** user accesses `figma.currentPage.selection`
- **THEN** system returns array of selected node proxies
#### Scenario: Setting selection
- **WHEN** user sets `figma.currentPage.selection = [node1, node2]`
- **THEN** system updates editor selection state
### Requirement: Component operations
The system SHALL provide component and instance methods.
#### Scenario: Creating component from node
- **WHEN** user calls `figma.createComponentFromNode(frame)`
- **THEN** system converts frame to COMPONENT and returns proxy
#### Scenario: Creating instance
- **WHEN** user calls `component.createInstance()`
- **THEN** system creates INSTANCE referencing the component
#### Scenario: Swapping instance
- **WHEN** user calls `instance.swapComponent(otherComponent)`
- **THEN** system updates instance's mainComponent reference
### Requirement: Grouping operations
The system SHALL provide grouping methods.
#### Scenario: Grouping nodes
- **WHEN** user calls `figma.group([node1, node2], parent)`
- **THEN** system creates a GROUP containing the nodes
#### Scenario: Ungrouping
- **WHEN** user calls `figma.ungroup(group)`
- **THEN** system removes group and reparents children to group's parent
### Requirement: Corner radius handling
The system SHALL handle both uniform and independent corner radii matching Figma's API.
#### Scenario: Uniform corner radius
- **WHEN** user accesses `rectangle.cornerRadius` and all corners have same radius
- **THEN** system returns that radius value
#### Scenario: Mixed corner radius
- **WHEN** user accesses `rectangle.cornerRadius` and corners have different radii
- **THEN** system returns the MIXED symbol
#### Scenario: Individual corners
- **WHEN** user accesses `rectangle.topLeftRadius`, `rectangle.topRightRadius`, etc.
- **THEN** system returns individual corner radius values
### Requirement: Cloning nodes
The system SHALL support deep cloning of nodes.
#### Scenario: Cloning a node
- **WHEN** user calls `node.clone()`
- **THEN** system creates a deep copy with new GUID and returns proxy
#### Scenario: Cloning with children
- **WHEN** user calls `frame.clone()` on a frame with children
- **THEN** system recursively clones children
### Requirement: JSON serialization
The system SHALL provide JSON export for AI tools and debugging.
#### Scenario: Exporting node to JSON
- **WHEN** user calls `figma.toJSON(node)`
- **THEN** system returns object with `type`, `name`, `id`, geometry, fills, strokes, and recursive children
#### Scenario: Exporting with depth limit
- **WHEN** user calls `figma.toJSON(node, { maxDepth: 2 })`
- **THEN** system includes children only 2 levels deep
### Requirement: Frozen arrays for safety
The system SHALL return frozen arrays for multi-value properties to prevent accidental mutation.
#### Scenario: Fills array is frozen
- **WHEN** user accesses `node.fills` and tries `fills.push(...)`
- **THEN** system throws error (array is frozen)
#### Scenario: Children array is frozen
- **WHEN** user accesses `parent.children` and tries `children[0] = other`
- **THEN** system throws error (array is frozen)
### Requirement: Internal symbols hidden
The system SHALL hide internal implementation details using Symbol properties.
#### Scenario: Internals not enumerable
- **WHEN** user calls `Object.keys(nodeProxy)` or `for (let k in nodeProxy)`
- **THEN** system does not expose INTERNAL_ID, INTERNAL_GRAPH, INTERNAL_API
### Requirement: Variable support
The system SHALL provide access to design variables.
#### Scenario: Listing variables
- **WHEN** user calls `figma.variables.getLocalVariables()`
- **THEN** system returns array of Variable objects
#### Scenario: Listing variable collections
- **WHEN** user calls `figma.variables.getLocalVariableCollections()`
- **THEN** system returns array of VariableCollection objects
#### Scenario: Getting variable by ID
- **WHEN** user calls `figma.variables.getVariableById("var-123")`
- **THEN** system returns Variable object or undefined
### Requirement: Type guards
The system SHALL provide type-checking methods matching Figma's API.
#### Scenario: Checking node type
- **WHEN** user checks `if (node.type === "FRAME")`
- **THEN** system allows type-based branching
### Requirement: Stub methods for unimplemented features
The system SHALL provide stub methods for Figma API methods not yet implemented, throwing descriptive errors.
#### Scenario: Calling unimplemented method
- **WHEN** user calls `figma.createImage(data)` or `figma.createShapeWithText()`
- **THEN** system throws "Not implemented: <method>"
#### Scenario: Notifying user
- **WHEN** user calls `figma.notify("Hello")`
- **THEN** system logs to console (headless mode has no UI notifications)

View file

@ -135,3 +135,179 @@ The test suite SHALL include tests for sceneNodeToJsx() covering shapes, text, l
#### Scenario: Export rectangle to JSX
- **WHEN** sceneNodeToJsx is called on a rectangle with blue fill
- **THEN** the output includes Rectangle component with bg prop
### Requirement: FigmaAPI unit tests
The project SHALL provide comprehensive unit tests for the Figma Plugin API in `tests/engine/figma-api.test.ts`.
#### Scenario: Node creation tests
- **WHEN** `bun test tests/engine/figma-api.test.ts` runs
- **THEN** tests verify `createFrame()`, `createRectangle()`, `createText()`, and other creation methods
#### Scenario: Property access tests
- **WHEN** tests read node properties (x, y, width, height, name, fills, strokes)
- **THEN** all getters return correct values from SceneNode
#### Scenario: Property setting tests
- **WHEN** tests set node properties (`node.name = "Test"`, `node.x = 100`)
- **THEN** SceneGraph is updated correctly
#### Scenario: Text property tests
- **WHEN** tests access `textNode.characters`, `fontSize`, `fontName`
- **THEN** text-specific properties work correctly
#### Scenario: Auto-layout tests
- **WHEN** tests set `layoutMode = "VERTICAL"`, `itemSpacing`, `paddingLeft`
- **THEN** auto-layout properties are applied
#### Scenario: Tree operation tests
- **WHEN** tests call `appendChild()`, `insertChild()`, `remove()`
- **THEN** scene graph tree is modified correctly
#### Scenario: Traversal tests
- **WHEN** tests call `findAll()`, `findOne()`, `findAllWithCriteria()`
- **THEN** correct nodes are returned
#### Scenario: Component tests
- **WHEN** tests create components and instances
- **THEN** component-instance relationships work
#### Scenario: Serialization tests
- **WHEN** tests call `figma.toJSON(node)`
- **THEN** JSON output includes all required properties
#### Scenario: Frozen array tests
- **WHEN** tests access `fills`, `strokes`, `children` and try to mutate
- **THEN** arrays are frozen and throw errors on mutation attempts
#### Scenario: 924 LOC of tests
- **WHEN** FigmaAPI test file is counted
- **THEN** it contains 924 lines covering 60+ test cases
### Requirement: Eval CLI integration tests
The project SHALL provide integration tests for eval command in `tests/engine/eval-cli.test.ts`.
#### Scenario: Inline code execution test
- **WHEN** test runs eval with `--code 'return figma.currentPage.id'`
- **THEN** output matches expected page ID
#### Scenario: Node creation test
- **WHEN** test runs eval creating a frame
- **THEN** document is modified and frame exists
#### Scenario: Write flag test
- **WHEN** test runs eval with `--write`
- **THEN** file is modified in-place
#### Scenario: Output file test
- **WHEN** test runs eval with `-o output.fig`
- **THEN** new file is created with modifications
#### Scenario: JSON output test
- **WHEN** test runs eval with `--json`
- **THEN** result is formatted as JSON
#### Scenario: Error handling test
- **WHEN** eval code throws error
- **THEN** stderr contains error message
#### Scenario: 202 LOC of integration tests
- **WHEN** eval CLI test file is counted
- **THEN** it contains 202 lines covering 17 integration scenarios
### Requirement: Tool schema unit tests
The project SHALL provide unit tests for unified tool definitions in `tests/engine/tools.test.ts`.
#### Scenario: Schema structure tests
- **WHEN** tests validate tool schemas
- **THEN** each tool has name, description, parameters, and handler
#### Scenario: Tool handler tests
- **WHEN** tests invoke tool handlers with valid params
- **THEN** handlers return expected results
#### Scenario: Parameter validation tests
- **WHEN** tests call handlers with invalid params
- **THEN** errors are raised or handled gracefully
#### Scenario: 390 LOC of tool tests
- **WHEN** tools test file is counted
- **THEN** it contains 390 lines
### Requirement: Tool adapter tests
The project SHALL provide adapter tests in `tests/engine/tools-ai-adapter.test.ts` (190 LOC) and `tests/engine/tools-cli.test.ts` (219 LOC).
#### Scenario: AI adapter format test
- **WHEN** AI adapter converts schema to LLM format
- **THEN** output matches expected structure
#### Scenario: CLI adapter test
- **WHEN** CLI commands use tool schema
- **THEN** parameters map correctly
### Requirement: App menu integration tests
The project SHALL provide integration tests for app menu in `tests/e2e/app-menu.spec.ts`.
#### Scenario: File menu test
- **WHEN** test clicks File menu items
- **THEN** actions (Open, Save, Export) are triggered
#### Scenario: Edit menu test
- **WHEN** test uses Edit menu (Undo, Redo, Copy, Paste)
- **THEN** operations execute correctly
#### Scenario: View menu test
- **WHEN** test uses View → Zoom in/out
- **THEN** viewport zoom changes
#### Scenario: Object menu test
- **WHEN** test uses Object → Group/Ungroup
- **THEN** nodes are grouped/ungrouped
#### Scenario: Text menu test
- **WHEN** test uses Text → Bold/Italic
- **THEN** text styles are applied
#### Scenario: Arrange menu test
- **WHEN** test uses Arrange → Bring to front
- **THEN** z-order changes
#### Scenario: 131 LOC of app menu tests
- **WHEN** app menu test file is counted
- **THEN** it contains 131 lines
### Requirement: Autosave integration tests
The project SHALL provide integration tests for autosave in `tests/e2e/autosave.spec.ts`.
#### Scenario: Autosave trigger test
- **WHEN** test modifies scene graph
- **THEN** autosave timer starts
#### Scenario: Debounce test
- **WHEN** test makes rapid changes
- **THEN** only one save occurs after 3 seconds
#### Scenario: Write test
- **WHEN** autosave timer expires
- **THEN** file is written
#### Scenario: Skip unchanged test
- **WHEN** sceneVersion matches savedVersion
- **THEN** write is skipped
#### Scenario: 113 LOC of autosave tests
- **WHEN** autosave test file is counted
- **THEN** it contains 113 lines
### Requirement: Test coverage expansion
The test suite SHALL expand from original coverage to include 2571 LOC of new tests across 7 files (figma-api.test.ts, eval-cli.test.ts, tools.test.ts, tools-ai-adapter.test.ts, tools-cli.test.ts, app-menu.spec.ts, autosave.spec.ts).
#### Scenario: Total new test lines
- **WHEN** new test files are counted
- **THEN** 2571 lines of tests are added

View file

@ -109,3 +109,92 @@ The project SHALL include a `test:coverage` script for measuring code coverage.
#### Scenario: Run coverage
- **WHEN** `bun run test:coverage` is run
- **THEN** test coverage metrics are reported
### Requirement: Unified tool definitions
The project SHALL define design tools once in `packages/core/src/tools/` and adapt them for AI, CLI, and MCP contexts.
#### Scenario: Canonical tool schema
- **WHEN** a new tool is added
- **THEN** it is defined in `packages/core/src/tools/schema.ts` as the single source of truth
#### Scenario: AI adapter
- **WHEN** AI assistant needs tool definitions
- **THEN** `packages/core/src/tools/ai-adapter.ts` converts schema to LLM-compatible format
#### Scenario: CLI adapter
- **WHEN** CLI commands use tools
- **THEN** citty commands consume tool schema directly
#### Scenario: MCP adapter (future)
- **WHEN** MCP server integration is added
- **THEN** MCP protocol adapter converts tool schema to MCP format
### Requirement: Tool schema structure
Tool schemas SHALL be defined in `packages/core/src/tools/schema.ts` with name, description, parameters (with types and descriptions), and handler function.
#### Scenario: Tool definition format
- **WHEN** defining a tool in schema.ts
- **THEN** structure includes `{ name, description, parameters: { <param>: { type, description } }, handler: (params) => result }`
#### Scenario: Type-safe parameters
- **WHEN** tool is invoked
- **THEN** parameters are validated against schema types
### Requirement: Deduplication of AI tools
The project SHALL eliminate duplication in `src/ai/tools.ts` by using `FigmaAPI.toJSON()` for node serialization and shared color parsing from `packages/core/src`.
#### Scenario: Node serialization
- **WHEN** AI tool returns node data
- **THEN** it uses `figmaAPI.toJSON(node)` instead of custom JSON builders
#### Scenario: Color parsing
- **WHEN** AI tool parses color input
- **THEN** it uses `parseColor()` from core instead of inline regex
#### Scenario: Code reduction
- **WHEN** AI tools are refactored
- **THEN** 311 lines are removed from `src/ai/tools.ts` via deduplication
### Requirement: Shared tool testing
The project SHALL provide test suites for tools in `tests/engine/tools.test.ts`, `tests/engine/tools-ai-adapter.test.ts`, and `tests/engine/tools-cli.test.ts`.
#### Scenario: Tool schema tests
- **WHEN** `tests/engine/tools.test.ts` runs
- **THEN** each tool schema is validated for structure and handler execution
#### Scenario: AI adapter tests
- **WHEN** `tests/engine/tools-ai-adapter.test.ts` runs
- **THEN** AI tool format conversion is verified
#### Scenario: CLI adapter tests
- **WHEN** `tests/engine/tools-cli.test.ts` runs
- **THEN** CLI command integration with tool schema is verified
### Requirement: Tool handler execution
Tool handlers SHALL receive parameters as plain objects and return structured results (success/error, data).
#### Scenario: Successful tool execution
- **WHEN** tool handler is invoked with valid params
- **THEN** it returns `{ success: true, data: <result> }`
#### Scenario: Tool execution error
- **WHEN** tool handler encounters error
- **THEN** it returns `{ success: false, error: "message" }`
### Requirement: Tool documentation
Each tool in schema.ts SHALL have clear description and parameter documentation for AI/human comprehension.
#### Scenario: Tool description
- **WHEN** AI queries available tools
- **THEN** description explains what the tool does (e.g., "Create a rectangle with specified dimensions and position")
#### Scenario: Parameter descriptions
- **WHEN** AI reads parameter schema
- **THEN** each parameter has type and description (e.g., `width: { type: 'number', description: 'Rectangle width in pixels' }`)

View file

@ -150,3 +150,95 @@ The VitePress sidebar SHALL include a "Comparison" link in the Guide section aft
- **WHEN** user views the Guide sidebar
- **THEN** a "Comparison" entry appears after "Tech Stack" linking to /guide/comparison
### Requirement: Eval command documentation
The docs site SHALL include comprehensive documentation for the eval command in `docs/eval-command.md`.
#### Scenario: Eval command page exists
- **WHEN** user navigates to `/eval-command`
- **THEN** full documentation for `open-pencil eval` command is displayed
#### Scenario: Overview section
- **WHEN** user reads eval command docs
- **THEN** overview explains purpose (headless scripting with Figma Plugin API)
#### Scenario: Usage examples
- **WHEN** user reads eval command docs
- **THEN** code examples show `--code`, `--stdin`, `--write`, `-o`, `--json` usage
#### Scenario: Architecture section
- **WHEN** user reads eval command docs
- **THEN** architecture diagram shows CLI → loadDocument → FigmaAPI → execute → serialize flow
#### Scenario: FigmaAPI surface coverage
- **WHEN** user reads eval command docs
- **THEN** documentation lists supported methods (createFrame, createRectangle, findAll, etc.)
#### Scenario: AI integration examples
- **WHEN** user reads eval command docs
- **THEN** examples show how AI tools use eval for batch operations
#### Scenario: Testing patterns
- **WHEN** user reads eval command docs
- **THEN** examples show headless testing with eval
#### Scenario: Migration from Figma plugins
- **WHEN** user reads eval command docs
- **THEN** guide explains how to adapt Figma plugin code to eval scripts
#### Scenario: 437 lines of documentation
- **WHEN** eval-command.md is counted
- **THEN** it contains 437 lines of comprehensive content
### Requirement: Comparison matrix updates
The docs site SHALL update comparison matrices to reflect new features (app menu, eval command, Figma Plugin API).
#### Scenario: Figma comparison update
- **WHEN** user reads `docs/guide/figma-comparison.md`
- **THEN** Interface & Navigation section includes app menu status
#### Scenario: Plugin API row
- **WHEN** user reads Figma comparison
- **THEN** a row documents eval command with Figma Plugin API compatibility
#### Scenario: AI tools update
- **WHEN** user reads Figma comparison
- **THEN** AI tools row is updated to reflect unified tool definitions and eval integration
#### Scenario: Penpot comparison update
- **WHEN** user reads `docs/guide/comparison.md`
- **THEN** Architecture section highlights headless scripting advantage (eval command with Plugin API that Penpot lacks)
### Requirement: App menu documentation
The docs site SHALL document app menu in appropriate guide pages.
#### Scenario: App menu in features
- **WHEN** user reads Features page
- **THEN** app menu is listed with menus (File, Edit, View, Object, Text, Arrange) and key actions
#### Scenario: Browser mode distinction
- **WHEN** user reads app menu docs
- **THEN** clarification that menu bar only appears in browser mode (Tauri uses native menus)
### Requirement: Autosave documentation
The docs site SHALL document autosave behavior in appropriate guide pages.
#### Scenario: Autosave in features
- **WHEN** user reads Features page or Getting Started
- **THEN** autosave is explained (3-second debounce, automatic for files with handle)
#### Scenario: Autosave limitations
- **WHEN** user reads autosave docs
- **THEN** note that new unsaved files don't autosave until user performs Save As
### Requirement: AGENTS.md update reference
The docs site SHALL reference tool unification in development pages when relevant.
#### Scenario: Tool architecture mention
- **WHEN** user reads development/contributing docs
- **THEN** unified tool definitions (`packages/core/src/tools/`) are mentioned as canonical source

View file

@ -22,7 +22,7 @@ export default defineConfig({
nav: [
{ text: 'User Guide', link: '/user-guide/' },
{ text: 'Reference', link: '/reference/keyboard-shortcuts' },
{ text: 'Development', link: '/guide/getting-started' },
{ text: 'Development', link: '/development/contributing' },
],
sidebar: {
@ -68,6 +68,7 @@ export default defineConfig({
{ text: 'MCP Tools', link: '/reference/mcp-tools' },
{ text: 'Scene Graph', link: '/reference/scene-graph' },
{ text: 'File Format', link: '/reference/file-format' },
{ text: 'Eval Command', link: '/eval-command' },
],
},
],

View file

@ -261,6 +261,10 @@ Open Pencil's approach is simpler and lower overhead. Penpot's approach is more
6. **Self-hosting** — Docker-based deployment for teams
7. **Maturity** — years of production usage, battle-tested at scale
## 11. Scripting & Extensibility
OpenPencil ships with an [`eval` command](/eval-command) that provides a Figma-compatible Plugin API for headless scripting — batch operations, automated testing, and AI-driven modifications all run without the GUI. Penpot has no plugin system or scripting API; extending it requires forking the Clojure backend.
## Summary
| Dimension | Winner | Why |

View file

@ -161,6 +161,14 @@ Right-clicking a node selects it first. Right-clicking empty canvas clears selec
Tauri v2 shell (~5MB vs Electron's ~100MB). Works fully offline — no account, no server, no internet required. Native menu bar with File/Edit/View/Object/Window/Help menus on all platforms. macOS gets an app-level submenu. Native Save/Open dialogs via Tauri plugin-dialog. Zstd compression offloaded to Rust for .fig export performance. Developer Tools accessible via <kbd>⌘</kbd><kbd>⌥</kbd><kbd>I</kbd>.
## App Menu (Browser)
In browser mode, a menu bar built with reka-ui Menubar provides access to all major editor actions. Six menus: **File** (Open, Save, Save As, Export selection), **Edit** (Undo, Redo, Copy, Paste, Duplicate, Delete, Select all), **View** (Zoom to fit, Zoom in/out, Toggle rulers), **Object** (Group, Ungroup, Frame selection, Create component, Create component set), **Text** (Font size adjustment, Bold, Italic, Underline, Strikethrough, Align submenu), **Arrange** (Bring to front, Send to back, Bring forward, Send backward). Keyboard shortcuts are displayed next to each menu item with platform-aware modifier labels (⌘ on Mac, Ctrl+ on Windows/Linux). Hidden when running in Tauri, which provides its own native menus.
## Autosave
Files are automatically saved 3 seconds after the last scene change. A debounced watcher monitors `sceneVersion` — multiple rapid edits only trigger a single write after activity settles. Uses the Tauri fs plugin on desktop or the File System Access API in supported browsers. Autosave is disabled for new untitled documents until the user performs an explicit Save As. Errors are handled silently — the user can always trigger a manual save with <kbd>⌘</kbd><kbd>S</kbd>.
## ScrubInput
All numeric inputs in the properties panel use a drag-to-scrub interaction — drag horizontally to adjust the value, or click to type directly. Supports suffix display (°, px, %).
@ -186,9 +194,12 @@ The engine is extracted to `packages/core/` (@open-pencil/core) — scene-graph,
- `open-pencil node <file> <id>` — detailed properties of a node by ID
- `open-pencil pages <file>` — list pages with node counts
- `open-pencil variables <file>` — list design variables and collections
- `open-pencil eval <file>` — execute JavaScript with Figma Plugin API
All commands support `--json` for machine-readable output. Runnable via `bun open-pencil` in the workspace. See [Project Structure](/development/contributing#project-structure) for the full monorepo layout.
The `eval` command deserves special mention: `bun open-pencil eval <file> --code '<js>'` executes JavaScript against a `.fig` file with a Figma-compatible `figma` global object. Enables headless scripting, batch operations, AI tool execution, and testing — all without the GUI. See [Eval Command](/eval-command) for the full reference.
## JSX Renderer
Programmatic design creation via TreeNode builder functions exported from `@open-pencil/core`: Frame, Text, Rectangle, Ellipse, and others. Supports Tailwind-like shorthand props — `w`, `h`, `bg`, `rounded`, `flex`, `gap`, `p`/`px`/`py`, `justify`, `items`, `shadow`, `blur`.
@ -205,7 +216,7 @@ Built-in AI assistant accessible via the AI tab in the properties panel or <kbd>
**Model selector** with curated models: Claude, Gemini, GPT, DeepSeek, Qwen, Kimi, Llama — stored in `@open-pencil/core` constants with benchmark-ranked tags. Responses stream as markdown (vue-stream-markdown).
**10 AI tools** wired to the editor store with valibot schemas: `create_shape`, `set_fill`, `set_stroke`, `update_node`, `set_layout`, `delete_node`, `select_nodes`, `get_page_tree`, `get_selection`, `rename_node`. The ToolLoopAgent executes tools automatically in a loop. Tool calls display as collapsible timeline entries in the chat (reka-ui Collapsible).
**10 AI tools** defined once in `packages/core/src/tools/schema.ts` as framework-agnostic `ToolDef` objects and adapted for AI via `toolsToAI()` with valibot schemas: `create_shape`, `set_fill`, `set_stroke`, `update_node`, `set_layout`, `delete_node`, `select_nodes`, `get_page_tree`, `get_selection`, `rename_node`. The same definitions power the CLI eval command via FigmaAPI. The ToolLoopAgent executes tools automatically in a loop. Tool calls display as collapsible timeline entries in the chat (reka-ui Collapsible).
Tested with Playwright using mock transport for CI.

View file

@ -6,7 +6,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
✅ Supported — feature works end-to-end · 🟡 Partial — core behavior exists, some sub-features missing · 🔲 Not yet implemented
:::
**Coverage:** 83 of 150 Figma feature items addressed — 64 ✅ fully supported, 19 🟡 partial, 67 🔲 not yet. Last updated: 2026-03-01.
**Coverage:** 85 of 152 Figma feature items addressed — 66 ✅ fully supported, 19 🟡 partial, 67 🔲 not yet. Last updated: 2026-03-01.
## Interface & Navigation
@ -27,7 +27,8 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Layer outlines view | 🔲 | Wireframe view of all layers |
| Custom file thumbnails | 🔲 | Thumbnail generated on export, but no custom thumbnail picker |
| Nudge value settings | 🔲 | Default 1px/10px; Figma allows custom small/big nudge values |
| AI tools | 🟡 | 10 AI tools via OpenRouter (create/modify/delete shapes, fills/strokes, layout); no AI-generated images or AI-powered search yet |
| App menu (browser mode) | ✅ | File, Edit, View, Object, Text, Arrange menus; Tauri uses native menus |
| AI tools | 🟡 | 10 tools via OpenRouter + unified tool definitions + eval command integration; no AI image generation yet |
## Layers & Shapes
@ -196,6 +197,12 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Version history | 🔲 | Browse and restore previous versions |
| Copy assets between tools | 🟡 | Figma clipboard works; no SVG/PDF clipboard |
## Plugin API & Scripting
| Feature | Status | Notes |
|---------|--------|-------|
| Eval command with Figma Plugin API | ✅ | Headless JavaScript execution with figma global object matching Figma's plugin surface |
## Collaboration & Dev Mode
| Feature | Status | Notes |