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,90 @@
1
+ /**
2
+ * insert-image — Apply an image fill override to a slide instance.
3
+ *
4
+ * Usage: node cli.mjs insert-image <file.deck> -o <output.deck> --slide <id|name> --key <overrideKey> --image <path.png> [--thumb <thumb.png>]
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid, parseId } from '../lib/node-helpers.mjs';
8
+ import { imageOv } from '../lib/image-helpers.mjs';
9
+ import { readFileSync, copyFileSync, existsSync, mkdirSync } from 'fs';
10
+ import { createHash } from 'crypto';
11
+ import { join, resolve } from 'path';
12
+ import { getImageDimensions, generateThumbnail } from '../lib/image-utils.mjs';
13
+
14
+ function sha1Hex(buf) {
15
+ return createHash('sha1').update(buf).digest('hex');
16
+ }
17
+
18
+ export async function run(args, flags) {
19
+ const file = args[0];
20
+ const outPath = flags.o || flags.output;
21
+ const slideRef = flags.slide;
22
+ const keyStr = flags.key;
23
+ const imagePath = flags.image;
24
+ const thumbPath = flags.thumb || null;
25
+
26
+ if (!file || !outPath || !slideRef || !keyStr || !imagePath) {
27
+ console.error('Usage: insert-image <file.deck> -o <out.deck> --slide <id|name> --key <key> --image <path.png> [--thumb <thumb.png>]');
28
+ process.exit(1);
29
+ }
30
+
31
+ const deck = await FigDeck.fromDeckFile(file);
32
+
33
+ // Find slide
34
+ const slide = findSlide(deck, slideRef);
35
+ if (!slide) { console.error(`Slide not found: ${slideRef}`); process.exit(1); }
36
+
37
+ const inst = deck.getSlideInstance(nid(slide));
38
+ if (!inst) { console.error(`No instance on slide ${nid(slide)}`); process.exit(1); }
39
+
40
+ const imgBuf = readFileSync(resolve(imagePath));
41
+ const imgHash = sha1Hex(imgBuf);
42
+ const { width, height } = await getImageDimensions(resolve(imagePath));
43
+
44
+ let thumbHash;
45
+ if (thumbPath) {
46
+ const tBuf = readFileSync(resolve(thumbPath));
47
+ thumbHash = sha1Hex(tBuf);
48
+ copyToImages(deck, thumbHash, resolve(thumbPath));
49
+ } else {
50
+ const tmpThumb = `/tmp/figmatk_thumb_${Date.now()}.png`;
51
+ await generateThumbnail(resolve(imagePath), tmpThumb);
52
+ thumbHash = sha1Hex(readFileSync(tmpThumb));
53
+ copyToImages(deck, thumbHash, tmpThumb);
54
+ }
55
+
56
+ // Copy full image to images dir
57
+ copyToImages(deck, imgHash, resolve(imagePath));
58
+
59
+ // Build and apply override
60
+ const key = parseId(keyStr);
61
+ const override = imageOv(key, imgHash, thumbHash, width, height);
62
+
63
+ if (!inst.symbolData) inst.symbolData = {};
64
+ if (!inst.symbolData.symbolOverrides) inst.symbolData.symbolOverrides = [];
65
+ inst.symbolData.symbolOverrides.push(override);
66
+
67
+ console.log(`Image: ${imgHash} (${width}×${height})`);
68
+ console.log(`Thumb: ${thumbHash}`);
69
+ console.log(`Applied to slide "${slide.name || nid(slide)}" key ${keyStr}`);
70
+
71
+ const bytes = await deck.saveDeck(outPath);
72
+ console.log(`Saved: ${outPath} (${bytes} bytes)`);
73
+ }
74
+
75
+ function copyToImages(deck, hash, srcPath) {
76
+ if (!deck.imagesDir) {
77
+ deck.imagesDir = `/tmp/figmatk_images_${Date.now()}`;
78
+ mkdirSync(deck.imagesDir, { recursive: true });
79
+ }
80
+ const dest = join(deck.imagesDir, hash);
81
+ if (!existsSync(dest)) {
82
+ copyFileSync(srcPath, dest);
83
+ }
84
+ }
85
+
86
+ function findSlide(deck, ref) {
87
+ const byId = deck.getNode(ref);
88
+ if (byId?.type === 'SLIDE') return byId;
89
+ return deck.getActiveSlides().find(s => s.name === ref);
90
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * inspect — Show document structure (node hierarchy tree).
3
+ *
4
+ * Usage: node cli.mjs inspect <file.deck|file.fig> [--depth N] [--type TYPE] [--json]
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid } from '../lib/node-helpers.mjs';
8
+
9
+ export async function run(args, flags) {
10
+ const file = args[0];
11
+ if (!file) { console.error('Usage: inspect <file.deck|file.fig>'); process.exit(1); }
12
+
13
+ const maxDepth = flags.depth ? parseInt(flags.depth) : Infinity;
14
+ const filterType = flags.type || null;
15
+ const jsonOut = flags.json != null;
16
+
17
+ const deck = file.endsWith('.fig')
18
+ ? FigDeck.fromFigFile(file)
19
+ : await FigDeck.fromDeckFile(file);
20
+
21
+ // Find root nodes (no parentIndex or parent not in nodeMap)
22
+ const roots = deck.message.nodeChanges.filter(n => {
23
+ if (!n.parentIndex?.guid) return true;
24
+ const pid = `${n.parentIndex.guid.sessionID}:${n.parentIndex.guid.localID}`;
25
+ return !deck.nodeMap.has(pid);
26
+ });
27
+
28
+ if (jsonOut) {
29
+ const collect = [];
30
+ for (const root of roots) {
31
+ collectJson(deck, nid(root), 0, maxDepth, filterType, collect);
32
+ }
33
+ console.log(JSON.stringify(collect, null, 2));
34
+ return;
35
+ }
36
+
37
+ // Summary
38
+ const slides = deck.getSlides();
39
+ const active = deck.getActiveSlides();
40
+ console.log(`Nodes: ${deck.message.nodeChanges.length} Slides: ${active.length} active / ${slides.length} total Blobs: ${deck.message.blobs?.length || 0}`);
41
+ if (deck.deckMeta) console.log(`Deck name: ${deck.deckMeta.file_name || '(unknown)'}`);
42
+ console.log('');
43
+
44
+ for (const root of roots) {
45
+ printTree(deck, nid(root), 0, maxDepth, filterType);
46
+ }
47
+ }
48
+
49
+ function printTree(deck, id, depth, maxDepth, filterType) {
50
+ if (depth > maxDepth) return;
51
+ const node = deck.getNode(id);
52
+ if (!node) return;
53
+
54
+ const type = node.type || '?';
55
+ const show = !filterType || type === filterType;
56
+
57
+ if (show) {
58
+ const indent = ' '.repeat(depth);
59
+ const name = node.name ? `"${node.name}"` : '';
60
+ const removed = node.phase === 'REMOVED' ? ' [REMOVED]' : '';
61
+ const sym = node.symbolData?.symbolID
62
+ ? ` sym=${node.symbolData.symbolID.sessionID}:${node.symbolData.symbolID.localID}`
63
+ : '';
64
+ const ovCount = node.symbolData?.symbolOverrides?.length;
65
+ const ovs = ovCount ? ` overrides=${ovCount}` : '';
66
+ console.log(`${indent}${type} ${name} (${id})${removed}${sym}${ovs}`);
67
+ }
68
+
69
+ for (const child of deck.getChildren(id)) {
70
+ printTree(deck, nid(child), depth + 1, maxDepth, filterType);
71
+ }
72
+ }
73
+
74
+ function collectJson(deck, id, depth, maxDepth, filterType, out) {
75
+ if (depth > maxDepth) return;
76
+ const node = deck.getNode(id);
77
+ if (!node) return;
78
+ const type = node.type || '?';
79
+ if (!filterType || type === filterType) {
80
+ out.push({
81
+ id, type, name: node.name || null,
82
+ phase: node.phase || null,
83
+ symbolID: node.symbolData?.symbolID ? nid({ guid: node.symbolData.symbolID }) : null,
84
+ overrides: node.symbolData?.symbolOverrides?.length || 0,
85
+ depth,
86
+ });
87
+ }
88
+ for (const child of deck.getChildren(id)) {
89
+ collectJson(deck, nid(child), depth + 1, maxDepth, filterType, out);
90
+ }
91
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * list-overrides — List all editable override keys per symbol.
3
+ *
4
+ * Usage: node cli.mjs list-overrides <file.deck> [--symbol NAME|ID]
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid } from '../lib/node-helpers.mjs';
8
+
9
+ export async function run(args, flags) {
10
+ const file = args[0];
11
+ if (!file) { console.error('Usage: list-overrides <file.deck>'); process.exit(1); }
12
+
13
+ const filterSym = flags.symbol || null;
14
+
15
+ const deck = file.endsWith('.fig')
16
+ ? FigDeck.fromFigFile(file)
17
+ : await FigDeck.fromDeckFile(file);
18
+
19
+ const symbols = deck.getSymbols();
20
+
21
+ for (const sym of symbols) {
22
+ const symId = nid(sym);
23
+ const symName = sym.name || '(unnamed)';
24
+
25
+ // Filter
26
+ if (filterSym && symName !== filterSym && symId !== filterSym) continue;
27
+
28
+ console.log(`\nSYMBOL "${symName}" (${symId})`);
29
+ console.log('─'.repeat(60));
30
+
31
+ // Walk children and find nodes with overrideKey
32
+ walkForOverrides(deck, symId, 1);
33
+ }
34
+ }
35
+
36
+ function walkForOverrides(deck, parentId, depth) {
37
+ const children = deck.getChildren(parentId);
38
+ for (const child of children) {
39
+ const id = nid(child);
40
+ const ok = child.overrideKey;
41
+
42
+ if (ok) {
43
+ const indent = ' '.repeat(depth);
44
+ const keyStr = `${ok.sessionID}:${ok.localID}`;
45
+ const type = child.type || '?';
46
+ const name = child.name || '';
47
+
48
+ let detail = '';
49
+ if (type === 'TEXT' && child.textData?.characters) {
50
+ const text = child.textData.characters;
51
+ const preview = text.length > 50 ? text.substring(0, 47) + '...' : text;
52
+ detail = ` → ${JSON.stringify(preview)}`;
53
+ } else if (type === 'ROUNDED_RECTANGLE' || type === 'RECTANGLE') {
54
+ const hasFill = child.fillPaints?.some(p => p.type === 'IMAGE');
55
+ detail = hasFill ? ' [IMAGE PLACEHOLDER]' : '';
56
+ } else if (type === 'INSTANCE') {
57
+ const sid = child.symbolData?.symbolID;
58
+ detail = sid ? ` sym=${sid.sessionID}:${sid.localID}` : '';
59
+ }
60
+
61
+ console.log(`${indent}${keyStr} ${type} "${name}"${detail}`);
62
+ }
63
+
64
+ walkForOverrides(deck, id, depth + 1);
65
+ }
66
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * list-text — List all text content in the deck.
3
+ *
4
+ * Usage: node cli.mjs list-text <file.deck>
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid } from '../lib/node-helpers.mjs';
8
+ import { hashToHex } from '../lib/image-helpers.mjs';
9
+
10
+ export async function run(args) {
11
+ const file = args[0];
12
+ if (!file) { console.error('Usage: list-text <file.deck>'); process.exit(1); }
13
+
14
+ const deck = file.endsWith('.fig')
15
+ ? FigDeck.fromFigFile(file)
16
+ : await FigDeck.fromDeckFile(file);
17
+
18
+ // Direct text nodes
19
+ console.log('=== Direct text nodes ===\n');
20
+ for (const node of deck.message.nodeChanges) {
21
+ if (node.type === 'TEXT' && node.textData?.characters) {
22
+ const text = node.textData.characters;
23
+ const preview = text.length > 80 ? text.substring(0, 77) + '...' : text;
24
+ console.log(`[${nid(node)}] "${node.name || ''}" → ${JSON.stringify(preview)}`);
25
+ }
26
+ }
27
+
28
+ // Override text per slide
29
+ console.log('\n=== Override text (per slide instance) ===\n');
30
+ const slides = deck.getActiveSlides();
31
+
32
+ for (const slide of slides) {
33
+ const inst = deck.getSlideInstance(nid(slide));
34
+ if (!inst) continue;
35
+
36
+ const symId = inst.symbolData?.symbolID;
37
+ const symStr = symId ? `${symId.sessionID}:${symId.localID}` : '?';
38
+ console.log(`SLIDE "${slide.name || nid(slide)}" → INSTANCE (${nid(inst)}) sym=${symStr}`);
39
+
40
+ const overrides = inst.symbolData?.symbolOverrides || [];
41
+ for (const ov of overrides) {
42
+ const path = (ov.guidPath?.guids || [])
43
+ .map(g => `${g.sessionID}:${g.localID}`).join(' → ');
44
+
45
+ if (ov.textData?.characters) {
46
+ const text = ov.textData.characters;
47
+ const preview = text.length > 80 ? text.substring(0, 77) + '...' : text;
48
+ console.log(` ${path} TEXT: ${JSON.stringify(preview)}`);
49
+ }
50
+ if (ov.fillPaints?.length) {
51
+ const paint = ov.fillPaints[0];
52
+ if (paint.type === 'IMAGE' && paint.image?.hash) {
53
+ const hex = hashToHex(paint.image.hash);
54
+ console.log(` ${path} IMAGE: ${hex} (${paint.originalImageWidth}×${paint.originalImageHeight})`);
55
+ }
56
+ }
57
+ }
58
+ console.log('');
59
+ }
60
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * remove-slide — Mark slides as REMOVED.
3
+ *
4
+ * Usage: node cli.mjs remove-slide <file.deck> -o <output.deck> --slide <id|name> [--slide ...]
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid, removeNode } from '../lib/node-helpers.mjs';
8
+
9
+ export async function run(args, flags) {
10
+ const file = args[0];
11
+ const outPath = flags.o || flags.output;
12
+ const slideRefs = Array.isArray(flags.slide) ? flags.slide : (flags.slide ? [flags.slide] : []);
13
+
14
+ if (!file || !outPath || slideRefs.length === 0) {
15
+ console.error('Usage: remove-slide <file.deck> -o <out.deck> --slide <id|name> [--slide ...]');
16
+ process.exit(1);
17
+ }
18
+
19
+ const deck = await FigDeck.fromDeckFile(file);
20
+
21
+ let removed = 0;
22
+ for (const ref of slideRefs) {
23
+ const slide = findSlide(deck, ref);
24
+ if (!slide) { console.error(`Slide not found: ${ref}`); continue; }
25
+
26
+ removeNode(slide);
27
+ console.log(` REMOVED slide "${slide.name || ''}" (${nid(slide)})`);
28
+
29
+ // Also remove child instances
30
+ for (const child of deck.getChildren(nid(slide))) {
31
+ removeNode(child);
32
+ console.log(` REMOVED child ${child.type} (${nid(child)})`);
33
+ }
34
+ removed++;
35
+ }
36
+
37
+ console.log(`\nRemoved ${removed} slide(s)`);
38
+
39
+ const bytes = await deck.saveDeck(outPath);
40
+ console.log(`Saved: ${outPath} (${bytes} bytes)`);
41
+ }
42
+
43
+ function findSlide(deck, ref) {
44
+ const byId = deck.getNode(ref);
45
+ if (byId?.type === 'SLIDE') return byId;
46
+ return deck.getActiveSlides().find(s => s.name === ref);
47
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * roundtrip — Decode and re-encode a deck with zero changes (pipeline validation).
3
+ *
4
+ * Usage: node cli.mjs roundtrip <file.deck> -o <output.deck>
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+
8
+ export async function run(args, flags) {
9
+ const file = args[0];
10
+ const outPath = flags.o || flags.output;
11
+
12
+ if (!file || !outPath) {
13
+ console.error('Usage: roundtrip <file.deck> -o <output.deck>');
14
+ process.exit(1);
15
+ }
16
+
17
+ console.log(`Reading: ${file}`);
18
+ const deck = await FigDeck.fromDeckFile(file);
19
+
20
+ const slides = deck.getSlides();
21
+ const active = deck.getActiveSlides();
22
+ const instances = deck.getInstances();
23
+ const symbols = deck.getSymbols();
24
+
25
+ console.log(` Nodes: ${deck.message.nodeChanges.length}`);
26
+ console.log(` Slides: ${active.length} active / ${slides.length} total`);
27
+ console.log(` Instances: ${instances.length}`);
28
+ console.log(` Symbols: ${symbols.length}`);
29
+ console.log(` Blobs: ${deck.message.blobs?.length || 0}`);
30
+ console.log(` Chunks: ${deck.rawFiles.length}`);
31
+ if (deck.deckMeta) console.log(` Deck name: ${deck.deckMeta.file_name || '(unknown)'}`);
32
+
33
+ console.log(`\nEncoding...`);
34
+ const bytes = await deck.saveDeck(outPath);
35
+ console.log(`Saved: ${outPath} (${bytes} bytes)`);
36
+ console.log(`\nRoundtrip complete. Open in Figma to verify.`);
37
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * update-text — Apply text overrides to an instance on a slide.
3
+ *
4
+ * Usage: node cli.mjs update-text <file.deck> -o <output.deck> --slide <id|name> --set key=value [--set key=value ...]
5
+ */
6
+ import { FigDeck } from '../lib/fig-deck.mjs';
7
+ import { nid, parseId } from '../lib/node-helpers.mjs';
8
+
9
+ export async function run(args, flags) {
10
+ const file = args[0];
11
+ const outPath = flags.o || flags.output;
12
+ const slideRef = flags.slide;
13
+ const sets = Array.isArray(flags.set) ? flags.set : (flags.set ? [flags.set] : []);
14
+
15
+ if (!file || !outPath || !slideRef || sets.length === 0) {
16
+ console.error('Usage: update-text <file.deck> -o <out.deck> --slide <id|name> --set key=value [--set ...]');
17
+ process.exit(1);
18
+ }
19
+
20
+ const deck = await FigDeck.fromDeckFile(file);
21
+
22
+ // Find slide by ID or name
23
+ const slide = findSlide(deck, slideRef);
24
+ if (!slide) { console.error(`Slide not found: ${slideRef}`); process.exit(1); }
25
+
26
+ const inst = deck.getSlideInstance(nid(slide));
27
+ if (!inst) { console.error(`No instance found on slide ${nid(slide)}`); process.exit(1); }
28
+
29
+ // Ensure symbolOverrides exists
30
+ if (!inst.symbolData) inst.symbolData = {};
31
+ if (!inst.symbolData.symbolOverrides) inst.symbolData.symbolOverrides = [];
32
+
33
+ let updated = 0;
34
+ for (const pair of sets) {
35
+ const eqIdx = pair.indexOf('=');
36
+ if (eqIdx < 0) { console.error(`Invalid --set format: ${pair}`); continue; }
37
+ const keyStr = pair.substring(0, eqIdx);
38
+ let value = pair.substring(eqIdx + 1);
39
+
40
+ // Empty string → space (prevents Figma crash)
41
+ if (value === '') value = ' ';
42
+
43
+ const key = parseId(keyStr);
44
+ const overrides = inst.symbolData.symbolOverrides;
45
+
46
+ // Find existing override for this key
47
+ const existing = overrides.find(o =>
48
+ o.guidPath?.guids?.length === 1 &&
49
+ o.guidPath.guids[0].sessionID === key.sessionID &&
50
+ o.guidPath.guids[0].localID === key.localID &&
51
+ o.textData
52
+ );
53
+
54
+ if (existing) {
55
+ existing.textData.characters = value;
56
+ } else {
57
+ overrides.push({
58
+ guidPath: { guids: [key] },
59
+ textData: { characters: value },
60
+ });
61
+ }
62
+ updated++;
63
+ console.log(` ${keyStr} → ${JSON.stringify(value.substring(0, 60))}`);
64
+ }
65
+
66
+ console.log(`Updated ${updated} text override(s) on slide "${slide.name || nid(slide)}"`);
67
+
68
+ const bytes = await deck.saveDeck(outPath);
69
+ console.log(`Saved: ${outPath} (${bytes} bytes)`);
70
+ }
71
+
72
+ function findSlide(deck, ref) {
73
+ // Try as ID first
74
+ const byId = deck.getNode(ref);
75
+ if (byId?.type === 'SLIDE') return byId;
76
+
77
+ // Try as name
78
+ return deck.getActiveSlides().find(s => s.name === ref);
79
+ }