figmatk 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +24 -0
- package/.claude-plugin/plugin.json +11 -0
- package/.mcp.json +8 -0
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/cli.mjs +101 -0
- package/commands/clone-slide.mjs +153 -0
- package/commands/insert-image.mjs +90 -0
- package/commands/inspect.mjs +91 -0
- package/commands/list-overrides.mjs +66 -0
- package/commands/list-text.mjs +60 -0
- package/commands/remove-slide.mjs +47 -0
- package/commands/roundtrip.mjs +37 -0
- package/commands/update-text.mjs +79 -0
- package/lib/api.mjs +2030 -0
- package/lib/blank-template.deck +0 -0
- package/lib/deep-clone.mjs +16 -0
- package/lib/fig-deck.mjs +307 -0
- package/lib/image-helpers.mjs +56 -0
- package/lib/image-utils.mjs +29 -0
- package/lib/node-helpers.mjs +49 -0
- package/mcp-server.mjs +251 -0
- package/package.json +63 -0
- package/skills/figmatk/SKILL.md +39 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "figmatk",
|
|
4
|
+
"description": "FigmaTK — Inspect and modify Figma Slides .deck files",
|
|
5
|
+
"metadata": {
|
|
6
|
+
"description": "FigmaTK — Inspect and modify Figma Slides .deck files natively"
|
|
7
|
+
},
|
|
8
|
+
"owner": {
|
|
9
|
+
"name": "FigmaTK Contributors",
|
|
10
|
+
"email": "shove-tank-malt@duck.com"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "figmatk",
|
|
15
|
+
"description": "Inspect and modify Figma Slides .deck files natively",
|
|
16
|
+
"version": "0.0.5",
|
|
17
|
+
"author": {
|
|
18
|
+
"name": "FigmaTK Contributors"
|
|
19
|
+
},
|
|
20
|
+
"source": "./",
|
|
21
|
+
"category": "development"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figmatk",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Inspect and modify Figma Slides .deck files natively — no lossy .pptx conversion",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "FigmaTK Contributors"
|
|
7
|
+
},
|
|
8
|
+
"repository": "https://github.com/rcoenen/figmatk",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": ["figma", "deck", "slides", "presentation"]
|
|
11
|
+
}
|
package/.mcp.json
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FigmaTK Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# FigmaTK — Figma Toolkit (v0.0.6)
|
|
2
|
+
|
|
3
|
+
Swiss-army knife CLI for Figma `.deck` and `.fig` files. Parse, inspect, modify, and rebuild Figma Slides decks programmatically — no Figma API required.
|
|
4
|
+
|
|
5
|
+
## Figma File Formats
|
|
6
|
+
|
|
7
|
+
Each Figma product has its own native file format:
|
|
8
|
+
|
|
9
|
+
| Product | Extension |
|
|
10
|
+
|---------|-----------|
|
|
11
|
+
| Figma Design | `.fig` |
|
|
12
|
+
| Figma Slides | `.deck` |
|
|
13
|
+
| Figma Jam (whiteboard) | `.jam` |
|
|
14
|
+
| Figma Buzz | `.buzz` |
|
|
15
|
+
| Figma Sites | `.site` |
|
|
16
|
+
| Figma Make | `.make` |
|
|
17
|
+
|
|
18
|
+
The `.deck` format borrows heavily from `.fig` — a `.deck` is essentially a ZIP containing a `.fig`-encoded canvas plus metadata and images. FigmaTK focuses on `.deck` files. We'll evaluate expanding to other formats as we go.
|
|
19
|
+
|
|
20
|
+
## Why native `.deck`?
|
|
21
|
+
|
|
22
|
+
Figma Slides lets you download presentations as `.deck` files — and re-upload them. This is the **native** round-trip format. The alternative, exporting to `.pptx`, is lossy: vectors get rasterized to bitmaps, fonts fall back to system defaults, and precise layout breaks. By staying in `.deck`, you preserve everything — fonts, vector shapes, component overrides, styles — exactly as Figma renders them.
|
|
23
|
+
|
|
24
|
+
**FigmaTK** makes this round-trip programmable. Download a `.deck`, modify it with these utilities, re-upload to Figma. Everything stays native.
|
|
25
|
+
|
|
26
|
+
Plug in [Claude Code](https://claude.ai/code), [Codex](https://openai.com/index/openai-codex/), or any coding agent and you have an AI that can read and edit Figma presentations end-to-end — without ever opening the Figma UI.
|
|
27
|
+
|
|
28
|
+
## Use Cases
|
|
29
|
+
|
|
30
|
+
- **AI agent for presentations** — let an LLM read slide content, rewrite copy, insert images, and produce a ready-to-upload `.deck` without ever touching the Figma UI
|
|
31
|
+
- **Batch-produce branded decks** — start from a company template, feed in data per client/project, get pixel-perfect slides out
|
|
32
|
+
- **Inspect and audit** — understand the internal structure of any `.deck` or `.fig` file
|
|
33
|
+
- **Automate** text and image placement across dozens of slides in seconds
|
|
34
|
+
- **Validate** your pipeline with lossless roundtrip encode/decode
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g figmatk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
No build step. Pure ESM (`.mjs`). Node 18+.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# See what's inside a deck
|
|
48
|
+
figmatk inspect my-presentation.deck
|
|
49
|
+
|
|
50
|
+
# List every text field and image in every slide
|
|
51
|
+
figmatk list-text my-presentation.deck
|
|
52
|
+
|
|
53
|
+
# Discover all override keys (what you can edit)
|
|
54
|
+
figmatk list-overrides my-presentation.deck
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### `inspect` — Document structure
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
figmatk inspect file.deck [--depth N] [--type TYPE] [--json]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Prints the full node hierarchy tree:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Nodes: 1314 Slides: 10 active / 10 total Blobs: 465
|
|
69
|
+
|
|
70
|
+
DOCUMENT "Document" (0:0)
|
|
71
|
+
CANVAS "Page 1" (0:1)
|
|
72
|
+
SLIDE_GRID "Presentation" (0:3)
|
|
73
|
+
SLIDE_ROW "Row" (1:1563)
|
|
74
|
+
SLIDE "1" (1:1559)
|
|
75
|
+
INSTANCE "Cover" (1:1564) sym=1:1322 overrides=3
|
|
76
|
+
SLIDE "2" (1:1570)
|
|
77
|
+
INSTANCE "Content" (1:1572) sym=1:1129 overrides=7
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Filter by node type (`--type SLIDE`, `--type INSTANCE`, `--type SYMBOL`) or limit depth. Use `--json` for machine-readable output.
|
|
81
|
+
|
|
82
|
+
### `list-text` — All content
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
figmatk list-text file.deck
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Shows every text string and image hash in the deck — both direct node text and symbol override text. Useful for auditing content or extracting copy.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
SLIDE "1" → INSTANCE (1:2001) sym=1:1322
|
|
92
|
+
57:48 TEXT: "My Presentation Title"
|
|
93
|
+
57:49 TEXT: "Subtitle Goes Here"
|
|
94
|
+
75:126 IMAGE: 780960f6236bd1305ceeb2590ca395e36e705816 (1011x621)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `list-overrides` — Editable fields
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
figmatk list-overrides file.deck [--symbol "Symbol Name"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For every symbol (component) in the file, lists each node that has an `overrideKey` — these are the fields you can modify via `symbolOverrides`. Shows the key ID, node type, name, and current default value.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
SYMBOL "Image+Text" (1:1205)
|
|
107
|
+
75:126 ROUNDED_RECTANGLE "Photo location" [IMAGE PLACEHOLDER]
|
|
108
|
+
75:127 TEXT "Header" → "Header"
|
|
109
|
+
75:131 TEXT "Subtitle" → "SUBTITLE 2"
|
|
110
|
+
75:132 TEXT "Body" → "Body small lorem ipsum..."
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `update-text` — Change text
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
figmatk update-text input.deck -o output.deck \
|
|
117
|
+
--slide 1:2000 \
|
|
118
|
+
--set "57:48=New Title" \
|
|
119
|
+
--set "57:49=New Subtitle"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Finds the slide (by node ID or name), locates its instance, and adds or updates text overrides. Repeat `--set` for multiple fields. Empty strings are auto-replaced with a space (empty string crashes Figma).
|
|
123
|
+
|
|
124
|
+
### `insert-image` — Place images
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
figmatk insert-image input.deck -o output.deck \
|
|
128
|
+
--slide 1:2006 \
|
|
129
|
+
--key 75:126 \
|
|
130
|
+
--image screenshot.png \
|
|
131
|
+
[--thumb thumbnail.png]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Overrides an image placeholder on a slide instance. Automatically:
|
|
135
|
+
- SHA-1 hashes the image and copies it to the `images/` directory
|
|
136
|
+
- Generates a ~320px thumbnail (or uses `--thumb` if provided)
|
|
137
|
+
- Sets the required `styleIdForFill` sentinel GUID
|
|
138
|
+
- Sets `imageThumbnail` with the thumbnail hash
|
|
139
|
+
|
|
140
|
+
### `clone-slide` — Duplicate with content
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
figmatk clone-slide input.deck -o output.deck \
|
|
144
|
+
--template 1:1559 \
|
|
145
|
+
--name "New Slide" \
|
|
146
|
+
--set "57:48=Title" \
|
|
147
|
+
--set "57:49=Subtitle" \
|
|
148
|
+
--set-image "75:126=photo.png"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Deep-clones a slide + instance pair from a template, assigns fresh GUIDs, applies text and image overrides, and appends to the deck. Uses `Uint8Array`-safe cloning (not `JSON.parse/stringify`).
|
|
152
|
+
|
|
153
|
+
### `remove-slide` — Delete slides
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
figmatk remove-slide input.deck -o output.deck \
|
|
157
|
+
--slide 1:1769 \
|
|
158
|
+
--slide 1:1732
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Marks slides and their child instances as `REMOVED`. Repeat `--slide` for multiple. Nodes are never deleted from the array — Figma requires them to remain with `phase: 'REMOVED'`.
|
|
162
|
+
|
|
163
|
+
### `roundtrip` — Validate the pipeline
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
figmatk roundtrip input.deck -o output.deck
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Decodes and re-encodes with zero changes. If Figma opens the output, your pipeline is sound. Prints node/slide/blob counts.
|
|
170
|
+
|
|
171
|
+
## Claude Cowork / Claude Code Integration
|
|
172
|
+
|
|
173
|
+
FigmaTK ships as a **Cowork plugin** with an MCP server. This lets Claude manipulate `.deck` files directly as tool calls.
|
|
174
|
+
|
|
175
|
+
### Install as plugin
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
claude plugin marketplace add rcoenen/figmatk
|
|
179
|
+
claude plugin install figmatk
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Or add as MCP server manually
|
|
183
|
+
|
|
184
|
+
In Claude Desktop → Settings → Developer → Edit Config:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"mcpServers": {
|
|
189
|
+
"figmatk": {
|
|
190
|
+
"command": "figmatk-mcp"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Available MCP tools
|
|
197
|
+
|
|
198
|
+
| Tool | Description |
|
|
199
|
+
|------|-------------|
|
|
200
|
+
| `figmatk_inspect` | Show node hierarchy tree |
|
|
201
|
+
| `figmatk_list_text` | List all text and image content per slide |
|
|
202
|
+
| `figmatk_list_overrides` | List editable override keys per symbol |
|
|
203
|
+
| `figmatk_update_text` | Apply text overrides to a slide instance |
|
|
204
|
+
| `figmatk_insert_image` | Apply image fill override |
|
|
205
|
+
| `figmatk_clone_slide` | Duplicate a slide |
|
|
206
|
+
| `figmatk_remove_slide` | Mark a slide as REMOVED |
|
|
207
|
+
| `figmatk_roundtrip` | Decode and re-encode for validation |
|
|
208
|
+
|
|
209
|
+
## Using as a Library
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
import { FigDeck } from 'figmatk/deck';
|
|
213
|
+
import { ov, nestedOv, removeNode } from 'figmatk/node-helpers';
|
|
214
|
+
import { imageOv } from 'figmatk/image-helpers';
|
|
215
|
+
import { deepClone } from 'figmatk/deep-clone';
|
|
216
|
+
|
|
217
|
+
// Load
|
|
218
|
+
const deck = await FigDeck.fromDeckFile('template.deck');
|
|
219
|
+
|
|
220
|
+
// Explore
|
|
221
|
+
console.log(deck.getActiveSlides().length, 'slides');
|
|
222
|
+
console.log(deck.getSymbols().map(s => s.name));
|
|
223
|
+
|
|
224
|
+
// Walk the tree
|
|
225
|
+
deck.walkTree('0:0', (node, depth) => {
|
|
226
|
+
console.log(' '.repeat(depth) + node.type + ' ' + (node.name || ''));
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Find a slide's instance and read its overrides
|
|
230
|
+
const slide = deck.getActiveSlides()[0];
|
|
231
|
+
const inst = deck.getSlideInstance('1:2000');
|
|
232
|
+
console.log(inst.symbolData.symbolOverrides);
|
|
233
|
+
|
|
234
|
+
// Save
|
|
235
|
+
await deck.saveDeck('output.deck');
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Key classes and functions
|
|
239
|
+
|
|
240
|
+
| Module | Export | Description |
|
|
241
|
+
|--------|--------|-------------|
|
|
242
|
+
| `lib/fig-deck.mjs` | `FigDeck` | Core class — parse, query, encode, save |
|
|
243
|
+
| `lib/node-helpers.mjs` | `nid(node)` | Format node ID as `"sessionID:localID"` |
|
|
244
|
+
| | `parseId(str)` | Parse `"57:48"` to `{ sessionID, localID }` |
|
|
245
|
+
| | `ov(key, text)` | Build a text override for `symbolOverrides` |
|
|
246
|
+
| | `nestedOv(instKey, textKey, text)` | Text override for nested instances |
|
|
247
|
+
| | `removeNode(node)` | Mark node as REMOVED |
|
|
248
|
+
| `lib/image-helpers.mjs` | `imageOv(key, hash, thumbHash, w, h)` | Build a complete image fill override |
|
|
249
|
+
| | `hexToHash(hex)` / `hashToHex(arr)` | Convert between hex strings and `Uint8Array(20)` |
|
|
250
|
+
| `lib/deep-clone.mjs` | `deepClone(obj)` | `Uint8Array`-safe deep clone |
|
|
251
|
+
|
|
252
|
+
### FigDeck API
|
|
253
|
+
|
|
254
|
+
| Method | Returns | Description |
|
|
255
|
+
|--------|---------|-------------|
|
|
256
|
+
| `FigDeck.fromDeckFile(path)` | `Promise<FigDeck>` | Load from `.deck` ZIP |
|
|
257
|
+
| `FigDeck.fromFigFile(path)` | `FigDeck` | Load from raw `.fig` |
|
|
258
|
+
| `deck.getSlides()` | `node[]` | All SLIDE nodes |
|
|
259
|
+
| `deck.getActiveSlides()` | `node[]` | Non-REMOVED slides |
|
|
260
|
+
| `deck.getInstances()` | `node[]` | All INSTANCE nodes |
|
|
261
|
+
| `deck.getSymbols()` | `node[]` | All SYMBOL nodes |
|
|
262
|
+
| `deck.getNode(id)` | `node` | Lookup by `"s:l"` string |
|
|
263
|
+
| `deck.getChildren(id)` | `node[]` | Child nodes |
|
|
264
|
+
| `deck.getSlideInstance(slideId)` | `node` | INSTANCE child of a SLIDE |
|
|
265
|
+
| `deck.walkTree(rootId, fn)` | void | DFS traversal |
|
|
266
|
+
| `deck.maxLocalID()` | `number` | Highest ID in use |
|
|
267
|
+
| `deck.rebuildMaps()` | void | Re-index after mutations |
|
|
268
|
+
| `deck.encodeFig()` | `Promise<Uint8Array>` | Encode to `canvas.fig` binary |
|
|
269
|
+
| `deck.saveDeck(path, opts?)` | `Promise<number>` | Write complete `.deck` ZIP |
|
|
270
|
+
| `deck.saveFig(path)` | `Promise<void>` | Write raw `.fig` binary |
|
|
271
|
+
|
|
272
|
+
## `.deck` File Format
|
|
273
|
+
|
|
274
|
+
See **[docs/deck-format.md](docs/deck-format.md)** for the full binary format specification — archive structure, chunk layout, node types, symbol overrides, image override requirements, cloning rules, and all known format constraints.
|
|
275
|
+
|
|
276
|
+
## Architecture
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
figmatk/
|
|
280
|
+
cli.mjs # CLI entry point — arg parsing + subcommand dispatch
|
|
281
|
+
mcp-server.mjs # MCP server for Claude Cowork / Claude Code
|
|
282
|
+
lib/
|
|
283
|
+
fig-deck.mjs # FigDeck class — own binary parser, no third-party deps
|
|
284
|
+
deep-clone.mjs # Uint8Array-safe recursive deep clone
|
|
285
|
+
node-helpers.mjs # Node ID utils, override builders, removal helper
|
|
286
|
+
image-helpers.mjs # SHA-1 hash conversion, image override builder
|
|
287
|
+
commands/
|
|
288
|
+
inspect.mjs # Tree view of document structure
|
|
289
|
+
list-text.mjs # All text + image content per slide
|
|
290
|
+
list-overrides.mjs # Editable override keys per symbol
|
|
291
|
+
update-text.mjs # Set text overrides on a slide
|
|
292
|
+
insert-image.mjs # Image fill override with auto-thumbnail
|
|
293
|
+
clone-slide.mjs # Duplicate a slide with content
|
|
294
|
+
remove-slide.mjs # Mark slides REMOVED
|
|
295
|
+
roundtrip.mjs # Decode/re-encode validation
|
|
296
|
+
skills/
|
|
297
|
+
figmatk/SKILL.md # Cowork skill definition
|
|
298
|
+
.claude-plugin/
|
|
299
|
+
plugin.json # Cowork plugin manifest
|
|
300
|
+
marketplace.json # Plugin marketplace listing
|
|
301
|
+
.mcp.json # MCP server config
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Six npm packages: `kiwi-schema`, `fzstd`, `zstd-codec`, `pako`, `archiver`, `@modelcontextprotocol/sdk`.
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FigmaTK — Swiss-army knife for Figma .deck / .fig files.
|
|
4
|
+
*
|
|
5
|
+
* Usage: figmatk <command> [args...]
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* inspect Show document structure (node hierarchy tree)
|
|
9
|
+
* list-text List all text content in the deck
|
|
10
|
+
* list-overrides List all editable override keys per symbol
|
|
11
|
+
* update-text Apply text overrides to a slide instance
|
|
12
|
+
* insert-image Apply an image fill override to a slide instance
|
|
13
|
+
* clone-slide Duplicate a template slide with new content
|
|
14
|
+
* remove-slide Mark slides as REMOVED
|
|
15
|
+
* roundtrip Decode and re-encode (pipeline validation)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const COMMANDS = {
|
|
19
|
+
'inspect': './commands/inspect.mjs',
|
|
20
|
+
'list-text': './commands/list-text.mjs',
|
|
21
|
+
'list-overrides': './commands/list-overrides.mjs',
|
|
22
|
+
'update-text': './commands/update-text.mjs',
|
|
23
|
+
'insert-image': './commands/insert-image.mjs',
|
|
24
|
+
'clone-slide': './commands/clone-slide.mjs',
|
|
25
|
+
'remove-slide': './commands/remove-slide.mjs',
|
|
26
|
+
'roundtrip': './commands/roundtrip.mjs',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const command = process.argv[2];
|
|
30
|
+
|
|
31
|
+
if (!command || command === '--help' || command === '-h') {
|
|
32
|
+
console.log(`FigmaTK — Swiss-army knife for Figma .deck / .fig files\n`);
|
|
33
|
+
console.log('Commands:');
|
|
34
|
+
console.log(' inspect Show document structure (node hierarchy tree)');
|
|
35
|
+
console.log(' list-text List all text content in the deck');
|
|
36
|
+
console.log(' list-overrides List editable override keys per symbol');
|
|
37
|
+
console.log(' update-text Apply text overrides to a slide instance');
|
|
38
|
+
console.log(' insert-image Apply image fill override to a slide instance');
|
|
39
|
+
console.log(' clone-slide Duplicate a template slide with new content');
|
|
40
|
+
console.log(' remove-slide Mark slides as REMOVED');
|
|
41
|
+
console.log(' roundtrip Decode and re-encode (pipeline validation)');
|
|
42
|
+
console.log('\nUsage: figmatk <command> [args...]');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!COMMANDS[command]) {
|
|
47
|
+
console.error(`Unknown command: ${command}\nRun with --help for available commands.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse args: positional args + flags (--flag value, --flag=value)
|
|
52
|
+
const rawArgs = process.argv.slice(3);
|
|
53
|
+
const positional = [];
|
|
54
|
+
const flags = {};
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
57
|
+
const arg = rawArgs[i];
|
|
58
|
+
if (arg.startsWith('--')) {
|
|
59
|
+
const eqIdx = arg.indexOf('=');
|
|
60
|
+
let key, value;
|
|
61
|
+
if (eqIdx >= 0) {
|
|
62
|
+
key = arg.substring(2, eqIdx);
|
|
63
|
+
value = arg.substring(eqIdx + 1);
|
|
64
|
+
} else {
|
|
65
|
+
key = arg.substring(2);
|
|
66
|
+
// Peek ahead for value (unless next arg is also a flag)
|
|
67
|
+
if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('--')) {
|
|
68
|
+
value = rawArgs[++i];
|
|
69
|
+
} else {
|
|
70
|
+
value = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Support repeating flags (e.g. --set k=v --set k2=v2)
|
|
74
|
+
if (flags[key] !== undefined) {
|
|
75
|
+
if (!Array.isArray(flags[key])) flags[key] = [flags[key]];
|
|
76
|
+
flags[key].push(value);
|
|
77
|
+
} else {
|
|
78
|
+
flags[key] = value;
|
|
79
|
+
}
|
|
80
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
81
|
+
// Short flag like -o
|
|
82
|
+
const key = arg.substring(1);
|
|
83
|
+
if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('-')) {
|
|
84
|
+
flags[key] = rawArgs[++i];
|
|
85
|
+
} else {
|
|
86
|
+
flags[key] = true;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
positional.push(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run command
|
|
94
|
+
const mod = await import(COMMANDS[command]);
|
|
95
|
+
try {
|
|
96
|
+
await mod.run(positional, flags);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(`Error: ${err.message}`);
|
|
99
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clone-slide — Duplicate a template slide with new content.
|
|
3
|
+
*
|
|
4
|
+
* Usage: node cli.mjs clone-slide <file.deck> -o <output.deck>
|
|
5
|
+
* --template <slideId|name> --name <newName>
|
|
6
|
+
* [--after <slideId>] [--set key=value ...] [--set-image key=path ...]
|
|
7
|
+
*/
|
|
8
|
+
import { FigDeck } from '../lib/fig-deck.mjs';
|
|
9
|
+
import { nid, parseId, positionChar } from '../lib/node-helpers.mjs';
|
|
10
|
+
import { imageOv } from '../lib/image-helpers.mjs';
|
|
11
|
+
import { deepClone } from '../lib/deep-clone.mjs';
|
|
12
|
+
import { readFileSync, copyFileSync, existsSync, mkdirSync } from 'fs';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import { join, resolve } from 'path';
|
|
15
|
+
import { getImageDimensions, generateThumbnail } from '../lib/image-utils.mjs';
|
|
16
|
+
|
|
17
|
+
function sha1Hex(buf) {
|
|
18
|
+
return createHash('sha1').update(buf).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function run(args, flags) {
|
|
22
|
+
const file = args[0];
|
|
23
|
+
const outPath = flags.o || flags.output;
|
|
24
|
+
const templateRef = flags.template;
|
|
25
|
+
const newName = flags.name || 'New Slide';
|
|
26
|
+
const sets = Array.isArray(flags.set) ? flags.set : (flags.set ? [flags.set] : []);
|
|
27
|
+
const setImages = Array.isArray(flags['set-image']) ? flags['set-image'] : (flags['set-image'] ? [flags['set-image']] : []);
|
|
28
|
+
|
|
29
|
+
if (!file || !outPath || !templateRef) {
|
|
30
|
+
console.error('Usage: clone-slide <file.deck> -o <out.deck> --template <id|name> --name <name> [--set key=val ...] [--set-image key=path ...]');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const deck = await FigDeck.fromDeckFile(file);
|
|
35
|
+
|
|
36
|
+
// Find template slide
|
|
37
|
+
const tmplSlide = findSlide(deck, templateRef);
|
|
38
|
+
if (!tmplSlide) { console.error(`Template slide not found: ${templateRef}`); process.exit(1); }
|
|
39
|
+
|
|
40
|
+
const tmplInst = deck.getSlideInstance(nid(tmplSlide));
|
|
41
|
+
if (!tmplInst) { console.error(`No instance on template slide`); process.exit(1); }
|
|
42
|
+
|
|
43
|
+
// Find SLIDE_ROW parent
|
|
44
|
+
const slideRowId = tmplSlide.parentIndex?.guid
|
|
45
|
+
? `${tmplSlide.parentIndex.guid.sessionID}:${tmplSlide.parentIndex.guid.localID}`
|
|
46
|
+
: null;
|
|
47
|
+
|
|
48
|
+
// Generate new IDs
|
|
49
|
+
let nextId = deck.maxLocalID() + 1;
|
|
50
|
+
const slideId = nextId++;
|
|
51
|
+
const instId = nextId++;
|
|
52
|
+
|
|
53
|
+
// Clone slide node
|
|
54
|
+
const newSlide = deepClone(tmplSlide);
|
|
55
|
+
newSlide.guid = { sessionID: 1, localID: slideId };
|
|
56
|
+
newSlide.name = newName;
|
|
57
|
+
newSlide.phase = 'CREATED';
|
|
58
|
+
if (slideRowId) {
|
|
59
|
+
const activeCount = deck.getActiveSlides().length;
|
|
60
|
+
newSlide.parentIndex = {
|
|
61
|
+
guid: parseId(slideRowId),
|
|
62
|
+
position: positionChar(activeCount),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
delete newSlide.prototypeInteractions;
|
|
66
|
+
delete newSlide.slideThumbnailHash;
|
|
67
|
+
delete newSlide.editInfo;
|
|
68
|
+
|
|
69
|
+
// Clone instance
|
|
70
|
+
const newInst = deepClone(tmplInst);
|
|
71
|
+
newInst.guid = { sessionID: 1, localID: instId };
|
|
72
|
+
newInst.name = newName;
|
|
73
|
+
newInst.phase = 'CREATED';
|
|
74
|
+
newInst.parentIndex = { guid: { sessionID: 1, localID: slideId }, position: '!' };
|
|
75
|
+
newInst.symbolData = {
|
|
76
|
+
symbolID: deepClone(tmplInst.symbolData?.symbolID),
|
|
77
|
+
symbolOverrides: [],
|
|
78
|
+
uniformScaleFactor: 1,
|
|
79
|
+
};
|
|
80
|
+
delete newInst.derivedSymbolData;
|
|
81
|
+
delete newInst.derivedSymbolDataLayoutVersion;
|
|
82
|
+
delete newInst.editInfo;
|
|
83
|
+
|
|
84
|
+
// Apply text overrides
|
|
85
|
+
for (const pair of sets) {
|
|
86
|
+
const eqIdx = pair.indexOf('=');
|
|
87
|
+
if (eqIdx < 0) continue;
|
|
88
|
+
const key = parseId(pair.substring(0, eqIdx));
|
|
89
|
+
let value = pair.substring(eqIdx + 1);
|
|
90
|
+
if (value === '') value = ' ';
|
|
91
|
+
newInst.symbolData.symbolOverrides.push({
|
|
92
|
+
guidPath: { guids: [key] },
|
|
93
|
+
textData: { characters: value },
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Apply image overrides
|
|
98
|
+
for (const pair of setImages) {
|
|
99
|
+
const eqIdx = pair.indexOf('=');
|
|
100
|
+
if (eqIdx < 0) continue;
|
|
101
|
+
const key = parseId(pair.substring(0, eqIdx));
|
|
102
|
+
const imgPath = resolve(pair.substring(eqIdx + 1));
|
|
103
|
+
|
|
104
|
+
const imgBuf = readFileSync(imgPath);
|
|
105
|
+
const imgHash = sha1Hex(imgBuf);
|
|
106
|
+
const { width: w, height: h } = await getImageDimensions(imgPath);
|
|
107
|
+
|
|
108
|
+
const tmpThumb = `/tmp/figmatk_thumb_${Date.now()}.png`;
|
|
109
|
+
await generateThumbnail(imgPath, tmpThumb);
|
|
110
|
+
const thumbHash = sha1Hex(readFileSync(tmpThumb));
|
|
111
|
+
|
|
112
|
+
copyToImages(deck, imgHash, imgPath);
|
|
113
|
+
copyToImages(deck, thumbHash, tmpThumb);
|
|
114
|
+
|
|
115
|
+
newInst.symbolData.symbolOverrides.push(
|
|
116
|
+
imageOv(key, imgHash, thumbHash, w, h)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Set slide position
|
|
121
|
+
const activeSlides = deck.getActiveSlides();
|
|
122
|
+
if (newSlide.transform) {
|
|
123
|
+
newSlide.transform.m02 = activeSlides.length * 2160;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Push to nodeChanges
|
|
127
|
+
deck.message.nodeChanges.push(newSlide);
|
|
128
|
+
deck.message.nodeChanges.push(newInst);
|
|
129
|
+
deck.rebuildMaps();
|
|
130
|
+
|
|
131
|
+
console.log(`Cloned slide "${tmplSlide.name}" → "${newName}" (1:${slideId} + 1:${instId})`);
|
|
132
|
+
console.log(` ${sets.length} text override(s), ${setImages.length} image override(s)`);
|
|
133
|
+
|
|
134
|
+
const bytes = await deck.saveDeck(outPath);
|
|
135
|
+
console.log(`Saved: ${outPath} (${bytes} bytes)`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function copyToImages(deck, hash, srcPath) {
|
|
139
|
+
if (!deck.imagesDir) {
|
|
140
|
+
deck.imagesDir = `/tmp/figmatk_images_${Date.now()}`;
|
|
141
|
+
mkdirSync(deck.imagesDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
const dest = join(deck.imagesDir, hash);
|
|
144
|
+
if (!existsSync(dest)) {
|
|
145
|
+
copyFileSync(srcPath, dest);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function findSlide(deck, ref) {
|
|
150
|
+
const byId = deck.getNode(ref);
|
|
151
|
+
if (byId?.type === 'SLIDE') return byId;
|
|
152
|
+
return deck.getSlides().find(s => s.name === ref);
|
|
153
|
+
}
|