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.
@@ -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
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "figmatk": {
4
+ "command": "node",
5
+ "args": ["${CLAUDE_PLUGIN_ROOT}/mcp-server.mjs"]
6
+ }
7
+ }
8
+ }
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
+ }