@pedroab0/skills 1.0.1 → 1.2.0
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/README.md +19 -3
- package/bin/cli.js +175 -35
- package/figma-extractor/README.md +14 -6
- package/figma-extractor/SKILL.md +18 -16
- package/figma-extractor/figma-plugin/README.md +10 -5
- package/figma-extractor/figma-plugin/code.js +461 -87
- package/figma-extractor/figma-plugin/ui.html +76 -23
- package/figma-extractor/scripts/fetch_figma.js +465 -61
- package/figma-node-builder/README.md +1 -1
- package/figma-node-builder/SKILL.md +7 -8
- package/figma-node-builder/scripts/inspect_spec.js +15 -2
- package/figma-to-code/README.md +10 -4
- package/figma-to-code/SKILL.md +18 -16
- package/figma-to-code/scripts/fetch_figma.js +448 -53
- package/package.json +12 -3
- package/figma-extractor/demo/desktop_home.json +0 -2434
- package/figma-extractor/demo/dt_money_desktop_home.png +0 -0
- package/figma-extractor/demo/dt_money_preview.png +0 -0
- package/figma-extractor/demo/dt_money_selection.json +0 -472
- package/figma-extractor/demo/index.html +0 -188
- package/figma-extractor/demo/style.css +0 -360
- package/figma-extractor/figma-plugin/assets/cover-1920.png +0 -0
- package/figma-extractor/figma-plugin/assets/cover.jpg +0 -0
- package/figma-extractor/figma-plugin/assets/cover.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon-128.png +0 -0
- package/figma-extractor/figma-plugin/assets/icon.png +0 -0
- package/figma-extractor/scripts/test_fetch_figma.js +0 -601
- package/figma-node-builder/scripts/test_inspect_spec.js +0 -148
- package/figma-to-code/scripts/test_fetch_figma.js +0 -581
|
@@ -11,27 +11,138 @@ const { URL } = require('url');
|
|
|
11
11
|
// Environment and Auth Utilities
|
|
12
12
|
// -----------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
function resolveHomeDir() {
|
|
15
|
+
return (
|
|
16
|
+
process.env.HOME ||
|
|
17
|
+
process.env.USERPROFILE ||
|
|
18
|
+
(typeof os.homedir === 'function' ? os.homedir() : '') ||
|
|
19
|
+
''
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decodeEnvBuffer(buf) {
|
|
24
|
+
if (!Buffer.isBuffer(buf)) {
|
|
25
|
+
const str = String(buf || '');
|
|
26
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
27
|
+
}
|
|
28
|
+
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
|
|
29
|
+
return buf.toString('utf16le', 2);
|
|
30
|
+
}
|
|
31
|
+
const str = buf.toString('utf8');
|
|
32
|
+
return str.charCodeAt(0) === 0xfeff ? str.slice(1) : str;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getPowerShellProfilePaths(home) {
|
|
36
|
+
const paths = [];
|
|
37
|
+
if (process.env.PROFILE && process.env.PROFILE.toLowerCase().endsWith('.ps1')) {
|
|
38
|
+
paths.push(process.env.PROFILE);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const docRoots = [];
|
|
42
|
+
if (home) {
|
|
43
|
+
docRoots.push(
|
|
44
|
+
path.join(home, 'Documents'),
|
|
45
|
+
path.join(home, 'Documentos'),
|
|
46
|
+
path.join(home, 'OneDrive', 'Documents'),
|
|
47
|
+
path.join(home, 'OneDrive', 'Documentos'),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const oneDriveEnvs = [
|
|
52
|
+
process.env.OneDrive,
|
|
53
|
+
process.env.OneDriveConsumer,
|
|
54
|
+
process.env.OneDriveCommercial,
|
|
55
|
+
].filter(Boolean);
|
|
56
|
+
|
|
57
|
+
for (const od of oneDriveEnvs) {
|
|
58
|
+
docRoots.push(path.join(od, 'Documents'), path.join(od, 'Documentos'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shellDirs = ['PowerShell', 'WindowsPowerShell'];
|
|
62
|
+
const profileFiles = [
|
|
63
|
+
'Microsoft.PowerShell_profile.ps1',
|
|
64
|
+
'Microsoft.VSCode_profile.ps1',
|
|
65
|
+
'profile.ps1',
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const docRoot of docRoots) {
|
|
69
|
+
if (!fs.existsSync(docRoot)) continue;
|
|
70
|
+
for (const shellDir of shellDirs) {
|
|
71
|
+
const fullShellDir = path.join(docRoot, shellDir);
|
|
72
|
+
if (!fs.existsSync(fullShellDir)) continue;
|
|
73
|
+
for (const profileFile of profileFiles) {
|
|
74
|
+
paths.push(path.join(fullShellDir, profileFile));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (home) {
|
|
80
|
+
const xdgPsDir = path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'powershell');
|
|
81
|
+
if (fs.existsSync(xdgPsDir)) {
|
|
82
|
+
for (const profileFile of profileFiles) {
|
|
83
|
+
paths.push(path.join(xdgPsDir, profileFile));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Array.from(new Set(paths));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function loadEnv(options = {}) {
|
|
92
|
+
const cwd = options.cwd || process.cwd();
|
|
93
|
+
const home = options.home !== undefined ? options.home : resolveHomeDir();
|
|
94
|
+
|
|
95
|
+
const dotEnvCandidates = [
|
|
96
|
+
path.join(cwd, '.env'),
|
|
97
|
+
path.join(cwd, '.env.local'),
|
|
18
98
|
home ? path.join(home, '.env') : null,
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
|
|
101
|
+
const shellConfigCandidates = [
|
|
19
102
|
home ? path.join(home, '.zshrc') : null,
|
|
103
|
+
home ? path.join(home, '.zprofile') : null,
|
|
104
|
+
home ? path.join(home, '.zshenv') : null,
|
|
20
105
|
home ? path.join(home, '.bashrc') : null,
|
|
106
|
+
home ? path.join(home, '.bash_profile') : null,
|
|
107
|
+
home ? path.join(home, '.profile') : null,
|
|
108
|
+
...getPowerShellProfilePaths(home),
|
|
21
109
|
].filter(Boolean);
|
|
22
110
|
|
|
23
|
-
|
|
24
|
-
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const candidates = [];
|
|
113
|
+
for (const p of dotEnvCandidates) {
|
|
114
|
+
if (!seen.has(p)) {
|
|
115
|
+
seen.add(p);
|
|
116
|
+
candidates.push({ filePath: p, isShellConfig: false });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const p of shellConfigCandidates) {
|
|
120
|
+
if (!seen.has(p)) {
|
|
121
|
+
seen.add(p);
|
|
122
|
+
candidates.push({ filePath: p, isShellConfig: true });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const { filePath, isShellConfig } of candidates) {
|
|
127
|
+
if (fs.existsSync(filePath)) {
|
|
25
128
|
try {
|
|
26
|
-
const
|
|
27
|
-
const
|
|
129
|
+
const rawBuf = fs.readFileSync(filePath);
|
|
130
|
+
const envContent = decodeEnvBuffer(rawBuf);
|
|
131
|
+
if (isShellConfig && !/figma_/i.test(envContent)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const lines = envContent.split(/\r?\n/);
|
|
28
135
|
for (const line of lines) {
|
|
136
|
+
if (isShellConfig && !/figma_/i.test(line)) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
29
139
|
const parsed = parseEnvLine(line);
|
|
30
140
|
if (parsed && parsed.key) {
|
|
141
|
+
const isFigmaVar = /^FIGMA_/i.test(parsed.key);
|
|
142
|
+
const envKey = isFigmaVar ? parsed.key.toUpperCase() : parsed.key;
|
|
31
143
|
// For dot-env files, load any variable; for shell configs, only load FIGMA_ variables
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
process.env[parsed.key] = parsed.val;
|
|
144
|
+
if (!process.env[envKey] && (!isShellConfig || isFigmaVar)) {
|
|
145
|
+
process.env[envKey] = parsed.val;
|
|
35
146
|
}
|
|
36
147
|
}
|
|
37
148
|
}
|
|
@@ -44,16 +155,47 @@ function loadEnv() {
|
|
|
44
155
|
|
|
45
156
|
function parseEnvLine(line) {
|
|
46
157
|
if (!line || typeof line !== 'string') return null;
|
|
47
|
-
let trimmed = line.trim();
|
|
48
|
-
if (trimmed.startsWith('
|
|
49
|
-
|
|
158
|
+
let trimmed = line.replace(/^\uFEFF/, '').trim();
|
|
159
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
160
|
+
return null;
|
|
50
161
|
}
|
|
51
|
-
|
|
162
|
+
|
|
163
|
+
// Handle PowerShell [Environment]::SetEnvironmentVariable("KEY", "VAL", ...)
|
|
164
|
+
const setEnvMatch = trimmed.match(
|
|
165
|
+
/^\[(?:System\.)?Environment\]::SetEnvironmentVariable\(\s*(['"])([A-Za-z_][A-Za-z0-9_]*)\1\s*,\s*(['"])([^'"]*)\3/i,
|
|
166
|
+
);
|
|
167
|
+
if (setEnvMatch) {
|
|
168
|
+
return { key: setEnvMatch[2], val: setEnvMatch[4] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Strip Unix 'export ' or Windows CMD 'set ' prefix (requires whitespace after keyword)
|
|
172
|
+
if (/^export\s+/i.test(trimmed)) {
|
|
173
|
+
trimmed = trimmed.replace(/^export\s+/i, '').trim();
|
|
174
|
+
} else if (/^set\s+/i.test(trimmed)) {
|
|
175
|
+
trimmed = trimmed.replace(/^set\s+/i, '').trim();
|
|
176
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') && trimmed.includes('=')) {
|
|
177
|
+
trimmed = trimmed.slice(1, -1).trim();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!trimmed.includes('=')) {
|
|
52
182
|
return null;
|
|
53
183
|
}
|
|
54
184
|
|
|
55
185
|
const eqIdx = trimmed.indexOf('=');
|
|
56
|
-
|
|
186
|
+
let key = trimmed.slice(0, eqIdx).trim();
|
|
187
|
+
|
|
188
|
+
// Strip PowerShell $env:KEY or ${env:KEY} syntax
|
|
189
|
+
if (/^\$env:/i.test(key)) {
|
|
190
|
+
key = key.replace(/^\$env:/i, '').trim();
|
|
191
|
+
} else if (/^\$\{env:([^}]+)\}$/i.test(key)) {
|
|
192
|
+
key = key.replace(/^\$\{env:([^}]+)\}$/i, '$1').trim();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
57
199
|
let rest = trimmed.slice(eqIdx + 1).trim();
|
|
58
200
|
|
|
59
201
|
let val = '';
|
|
@@ -409,13 +551,234 @@ function normalizeComponentProps(componentProperties) {
|
|
|
409
551
|
// AST Pruning & Token Optimization
|
|
410
552
|
// -----------------------------------------------------------------------------
|
|
411
553
|
|
|
412
|
-
|
|
554
|
+
const VECTOR_PRIMITIVE_TYPES = new Set([
|
|
555
|
+
'VECTOR',
|
|
556
|
+
'BOOLEAN_OPERATION',
|
|
557
|
+
'STAR',
|
|
558
|
+
'LINE',
|
|
559
|
+
'REGULAR_POLYGON',
|
|
560
|
+
'POLYGON',
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
function cleanLayerName(rawName, fallback = 'Node') {
|
|
564
|
+
if (!rawName || typeof rawName !== 'string') return fallback;
|
|
565
|
+
const trimmed = rawName.trim();
|
|
566
|
+
if (!trimmed) return fallback;
|
|
567
|
+
if (/^data:image\//i.test(trimmed)) return 'Image';
|
|
568
|
+
if (/^(?:<svg\b|<\?xml\b)/i.test(trimmed)) return 'SVG';
|
|
569
|
+
if (/^https?:\/\//i.test(trimmed) && trimmed.length > 60) {
|
|
570
|
+
try {
|
|
571
|
+
const lastSeg = trimmed.split('?')[0].split('/').pop();
|
|
572
|
+
return lastSeg && lastSeg.length <= 60 ? lastSeg : 'Image';
|
|
573
|
+
} catch (_) {
|
|
574
|
+
return 'Image';
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const singleLine = trimmed.replace(/\s+/g, ' ');
|
|
578
|
+
return singleLine.length > 80 ? singleLine.slice(0, 80) + '…' : singleLine;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isRawAssetString(str) {
|
|
582
|
+
if (!str || typeof str !== 'string') return false;
|
|
583
|
+
const t = str.trim();
|
|
584
|
+
return /^data:image\//i.test(t) || /^(?:<svg\b|<\?xml\b)/i.test(t);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function nodeHasImageFill(node) {
|
|
588
|
+
return Array.isArray(node.fills) && node.fills.some((f) => f && f.visible !== false && f.type === 'IMAGE');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const GRAPHIC_NAME_RE =
|
|
592
|
+
/svg|icon|ícone|icone|logo|marca|brand|illustrat|ilustra|vector|vetor|symbol|simbol|símbolo|glyph|graphic|ornament|pattern|artwork/i;
|
|
593
|
+
|
|
594
|
+
function hasSvgExportSetting(node) {
|
|
595
|
+
if (!Array.isArray(node.exportSettings) || node.exportSettings.length === 0) return false;
|
|
596
|
+
return node.exportSettings.some((s) => s && String(s.format || '').toUpperCase() === 'SVG');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function subtreeHasNoInstances(node, maxNodes = 400) {
|
|
600
|
+
let visited = 0;
|
|
601
|
+
function walk(curr, isRoot) {
|
|
602
|
+
if (!curr || typeof curr !== 'object' || ++visited > maxNodes) return false;
|
|
603
|
+
const t = curr.type || '';
|
|
604
|
+
if (!isRoot && (t === 'INSTANCE' || t === 'COMPONENT' || t === 'COMPONENT_SET')) return false;
|
|
605
|
+
const kids = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
606
|
+
for (const k of kids) {
|
|
607
|
+
if (!walk(k, false)) return false;
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
return walk(node, true);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function analyzeVectorSubtree(node, varMap, maxNodes = 600) {
|
|
615
|
+
let visited = 0;
|
|
616
|
+
let vectorPrimitiveCount = 0;
|
|
617
|
+
let shapeCount = 0;
|
|
618
|
+
let plainRectCount = 0;
|
|
619
|
+
let primaryFill = null;
|
|
620
|
+
|
|
621
|
+
const rootType = node.type || '';
|
|
622
|
+
const hasSvgExport = hasSvgExportSetting(node);
|
|
623
|
+
|
|
624
|
+
// Preserve Auto-Layout wrapper frames with their own styling (e.g., Icon Badge) unless explicitly exported as SVG
|
|
625
|
+
if (rootType === 'FRAME' && !hasSvgExport && node.layoutMode && node.layoutMode !== 'NONE') {
|
|
626
|
+
const ownFills = simplifyFills(node.fills, node.boundVariables || {}, varMap);
|
|
627
|
+
const ownStrokes = simplifyStrokes(node.strokes, node.strokeWeight, node.strokeAlign, node.boundVariables || {}, varMap);
|
|
628
|
+
if (ownFills || ownStrokes || node.cornerRadius) {
|
|
629
|
+
return { isPure: false, primaryFill: null };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function check(curr, isRoot) {
|
|
634
|
+
if (!curr || typeof curr !== 'object' || ++visited > maxNodes) return false;
|
|
635
|
+
const t = curr.type || '';
|
|
636
|
+
if (t === 'TEXT' || t === 'SLICE' || t === 'STICKY' || t === 'TABLE') return false;
|
|
637
|
+
if (!isRoot && (t === 'INSTANCE' || t === 'COMPONENT' || t === 'COMPONENT_SET')) return false;
|
|
638
|
+
if (nodeHasImageFill(curr)) return false;
|
|
639
|
+
|
|
640
|
+
if (!primaryFill) {
|
|
641
|
+
const fills = simplifyFills(curr.fills, curr.boundVariables || {}, varMap);
|
|
642
|
+
const solidFill = fills && fills.find((f) => f.type === 'SOLID' && f.color);
|
|
643
|
+
if (solidFill) {
|
|
644
|
+
primaryFill = solidFill;
|
|
645
|
+
} else {
|
|
646
|
+
const strokes = simplifyStrokes(curr.strokes, curr.strokeWeight, curr.strokeAlign, curr.boundVariables || {}, varMap);
|
|
647
|
+
if (strokes && strokes.colors && strokes.colors[0]) {
|
|
648
|
+
const c0 = strokes.colors[0];
|
|
649
|
+
primaryFill = typeof c0 === 'object' ? { type: 'SOLID', ...c0 } : { type: 'SOLID', color: c0 };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (VECTOR_PRIMITIVE_TYPES.has(t)) {
|
|
655
|
+
vectorPrimitiveCount++;
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (t === 'ELLIPSE') {
|
|
660
|
+
shapeCount++;
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (t === 'RECTANGLE') {
|
|
665
|
+
if (curr.isMask === true || /clip|mask|vector|path|rect|bound|bg/i.test(String(curr.name || ''))) {
|
|
666
|
+
shapeCount++;
|
|
667
|
+
} else {
|
|
668
|
+
plainRectCount++;
|
|
669
|
+
}
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (t === 'GROUP' || t === 'FRAME' || (isRoot && (t === 'INSTANCE' || t === 'COMPONENT'))) {
|
|
674
|
+
if (!isRoot && t === 'FRAME' && curr.layoutMode && curr.layoutMode !== 'NONE') {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
const kids = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
678
|
+
if (kids.length === 0) return !isRoot;
|
|
679
|
+
for (const k of kids) {
|
|
680
|
+
if (!check(k, false)) return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const treeOk = check(node, true);
|
|
689
|
+
const w = node.absoluteBoundingBox ? node.absoluteBoundingBox.width : 9999;
|
|
690
|
+
const h = node.absoluteBoundingBox ? node.absoluteBoundingBox.height : 9999;
|
|
691
|
+
|
|
692
|
+
if (!treeOk) {
|
|
693
|
+
if (hasSvgExport && w <= 800 && h <= 800 && !nodeHasImageFill(node) && subtreeHasNoInstances(node)) {
|
|
694
|
+
return { isPure: true, primaryFill };
|
|
695
|
+
}
|
|
696
|
+
return { isPure: false, primaryFill: null };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const isSmallGraphic = w <= 160 && h <= 160;
|
|
700
|
+
const hasGraphicName = GRAPHIC_NAME_RE.test(String(node.name || ''));
|
|
701
|
+
|
|
702
|
+
const hasEnoughPrimitives =
|
|
703
|
+
vectorPrimitiveCount > 0 ||
|
|
704
|
+
(shapeCount + plainRectCount >= 2 && (hasSvgExport || (isSmallGraphic && hasGraphicName)));
|
|
705
|
+
|
|
706
|
+
const rectGuardOk =
|
|
707
|
+
plainRectCount === 0 ||
|
|
708
|
+
vectorPrimitiveCount >= 2 ||
|
|
709
|
+
isSmallGraphic ||
|
|
710
|
+
hasGraphicName ||
|
|
711
|
+
hasSvgExport;
|
|
712
|
+
|
|
713
|
+
const isPure = hasEnoughPrimitives && rectGuardOk;
|
|
714
|
+
return { isPure, primaryFill };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function analyzeMaskedImageSubtree(node) {
|
|
718
|
+
const t = node.type || '';
|
|
719
|
+
if (t !== 'GROUP' && t !== 'FRAME') return null;
|
|
720
|
+
if (node.layoutMode && node.layoutMode !== 'NONE') return null;
|
|
721
|
+
|
|
722
|
+
const kids = Array.isArray(node.children) ? node.children.filter((c) => c && c.visible !== false) : [];
|
|
723
|
+
if (kids.length === 0 || kids.length > 20) return null;
|
|
724
|
+
|
|
725
|
+
let foundImageFill = false;
|
|
726
|
+
let hasMaskOrClip = false;
|
|
727
|
+
|
|
728
|
+
function scan(curr) {
|
|
729
|
+
const ct = curr.type || '';
|
|
730
|
+
if (ct === 'TEXT' || ct === 'INSTANCE' || ct === 'COMPONENT') return false;
|
|
731
|
+
if (curr.isMask === true) hasMaskOrClip = true;
|
|
732
|
+
if (nodeHasImageFill(curr)) foundImageFill = true;
|
|
733
|
+
|
|
734
|
+
const sub = Array.isArray(curr.children) ? curr.children.filter((c) => c && c.visible !== false) : [];
|
|
735
|
+
for (const s of sub) {
|
|
736
|
+
if (!scan(s)) return false;
|
|
737
|
+
}
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!scan(node) || !foundImageFill) return null;
|
|
742
|
+
if (!hasMaskOrClip && !/mask|image|img|photo|avatar|banner|thumb/i.test(String(node.name || ''))) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function pruneFigmaNode(node, context = {}, options = {}, _depth = 0) {
|
|
413
749
|
if (!node || typeof node !== 'object') return node;
|
|
414
750
|
|
|
415
751
|
const { varMap, components, componentSets } = context;
|
|
752
|
+
const cleanName = cleanLayerName(node.name, 'Node');
|
|
753
|
+
const bound = node.boundVariables || {};
|
|
754
|
+
const bounds = node.absoluteBoundingBox
|
|
755
|
+
? {
|
|
756
|
+
width: Math.round(node.absoluteBoundingBox.width),
|
|
757
|
+
height: Math.round(node.absoluteBoundingBox.height),
|
|
758
|
+
}
|
|
759
|
+
: undefined;
|
|
760
|
+
|
|
761
|
+
// 1. Leaf Vector Primitives (VECTOR, BOOLEAN_OPERATION, STAR, LINE, POLYGON)
|
|
762
|
+
if (VECTOR_PRIMITIVE_TYPES.has(node.type)) {
|
|
763
|
+
const vecNode = { name: cleanName, type: 'VECTOR' };
|
|
764
|
+
if (options.includeIds && node.id) vecNode.id = node.id;
|
|
765
|
+
if (node.visible === false) vecNode.visible = false;
|
|
766
|
+
if (bounds) vecNode.bounds = bounds;
|
|
767
|
+
const fills = simplifyFills(node.fills, bound, varMap);
|
|
768
|
+
if (fills) {
|
|
769
|
+
vecNode.fills = fills;
|
|
770
|
+
} else {
|
|
771
|
+
const strokes = simplifyStrokes(node.strokes, node.strokeWeight, node.strokeAlign, bound, varMap);
|
|
772
|
+
if (strokes && strokes.colors && strokes.colors[0]) {
|
|
773
|
+
const c0 = strokes.colors[0];
|
|
774
|
+
vecNode.fills = [typeof c0 === 'object' ? { type: 'SOLID', ...c0 } : { type: 'SOLID', color: c0 }];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return vecNode;
|
|
778
|
+
}
|
|
416
779
|
|
|
417
780
|
const pruned = {
|
|
418
|
-
name:
|
|
781
|
+
name: cleanName,
|
|
419
782
|
type: node.type,
|
|
420
783
|
};
|
|
421
784
|
|
|
@@ -425,16 +788,14 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
425
788
|
|
|
426
789
|
if (node.visible === false) pruned.visible = false;
|
|
427
790
|
|
|
428
|
-
const bound = node.boundVariables || {};
|
|
429
|
-
|
|
430
791
|
// Component Instance & Master Resolution
|
|
431
792
|
if (node.type === 'INSTANCE') {
|
|
432
793
|
if (node.componentId && components && components[node.componentId]) {
|
|
433
794
|
const master = components[node.componentId];
|
|
434
795
|
const set = master.componentSetId && componentSets ? componentSets[master.componentSetId] : null;
|
|
435
796
|
pruned.component = {
|
|
436
|
-
name: set ? set.name : master.name,
|
|
437
|
-
variant: master.name,
|
|
797
|
+
name: cleanLayerName(set ? set.name : master.name, cleanName),
|
|
798
|
+
variant: cleanLayerName(master.name, cleanName),
|
|
438
799
|
};
|
|
439
800
|
if (options.includeIds) {
|
|
440
801
|
pruned.component.id = node.componentId;
|
|
@@ -446,12 +807,7 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
446
807
|
|
|
447
808
|
// Shallow instances: skip internal rendering sub-layers if requested
|
|
448
809
|
if (options.shallowInstances) {
|
|
449
|
-
if (
|
|
450
|
-
pruned.bounds = {
|
|
451
|
-
width: Math.round(node.absoluteBoundingBox.width),
|
|
452
|
-
height: Math.round(node.absoluteBoundingBox.height),
|
|
453
|
-
};
|
|
454
|
-
}
|
|
810
|
+
if (bounds) pruned.bounds = bounds;
|
|
455
811
|
return pruned;
|
|
456
812
|
}
|
|
457
813
|
} else if (node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
|
|
@@ -459,11 +815,42 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
459
815
|
if (props) pruned.props = props;
|
|
460
816
|
}
|
|
461
817
|
|
|
818
|
+
// 2. Collapse Pure SVG / Icon / Vector Graphic Containers
|
|
819
|
+
if (_depth > 0 || node.type === 'GROUP' || node.type === 'INSTANCE') {
|
|
820
|
+
const vecCheck = analyzeVectorSubtree(node, varMap);
|
|
821
|
+
if (vecCheck.isPure) {
|
|
822
|
+
if (node.type !== 'INSTANCE' && node.type !== 'COMPONENT') {
|
|
823
|
+
pruned.type = 'VECTOR';
|
|
824
|
+
pruned.isSvg = true;
|
|
825
|
+
}
|
|
826
|
+
if (bounds) pruned.bounds = bounds;
|
|
827
|
+
const ownFills = simplifyFills(node.fills, bound, varMap);
|
|
828
|
+
if (ownFills) {
|
|
829
|
+
pruned.fills = ownFills;
|
|
830
|
+
} else if (vecCheck.primaryFill) {
|
|
831
|
+
pruned.fills = [vecCheck.primaryFill];
|
|
832
|
+
}
|
|
833
|
+
return pruned;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// 3. Collapse Masked Image Groups
|
|
838
|
+
if (_depth > 0 && analyzeMaskedImageSubtree(node)) {
|
|
839
|
+
if (bounds) pruned.bounds = bounds;
|
|
840
|
+
pruned.fills = [{ type: 'IMAGE' }];
|
|
841
|
+
if (node.cornerRadius) pruned.cornerRadius = node.cornerRadius;
|
|
842
|
+
return pruned;
|
|
843
|
+
}
|
|
844
|
+
|
|
462
845
|
// Layout & Flexbox properties
|
|
463
846
|
if (node.layoutMode && node.layoutMode !== 'NONE') {
|
|
464
847
|
pruned.layoutMode = node.layoutMode;
|
|
465
|
-
if (node.primaryAxisAlignItems
|
|
466
|
-
|
|
848
|
+
if (node.primaryAxisAlignItems && node.primaryAxisAlignItems !== 'MIN') {
|
|
849
|
+
pruned.primaryAxisAlignItems = node.primaryAxisAlignItems;
|
|
850
|
+
}
|
|
851
|
+
if (node.counterAxisAlignItems && node.counterAxisAlignItems !== 'MIN') {
|
|
852
|
+
pruned.counterAxisAlignItems = node.counterAxisAlignItems;
|
|
853
|
+
}
|
|
467
854
|
|
|
468
855
|
if (node.itemSpacing) {
|
|
469
856
|
pruned.itemSpacing = node.itemSpacing;
|
|
@@ -484,18 +871,15 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
484
871
|
if (padToken) pruned.paddingToken = padToken;
|
|
485
872
|
}
|
|
486
873
|
|
|
487
|
-
if (node.layoutWrap) pruned.layoutWrap = node.layoutWrap;
|
|
874
|
+
if (node.layoutWrap && node.layoutWrap !== 'NO_WRAP') pruned.layoutWrap = node.layoutWrap;
|
|
488
875
|
}
|
|
489
876
|
|
|
490
877
|
if (node.layoutGrow) pruned.layoutGrow = node.layoutGrow;
|
|
491
|
-
if (node.layoutAlign) pruned.layoutAlign = node.layoutAlign;
|
|
878
|
+
if (node.layoutAlign && node.layoutAlign !== 'INHERIT') pruned.layoutAlign = node.layoutAlign;
|
|
492
879
|
|
|
493
880
|
// Dimensions & Coordinates
|
|
494
|
-
if (
|
|
495
|
-
pruned.bounds =
|
|
496
|
-
width: Math.round(node.absoluteBoundingBox.width),
|
|
497
|
-
height: Math.round(node.absoluteBoundingBox.height),
|
|
498
|
-
};
|
|
881
|
+
if (bounds) {
|
|
882
|
+
pruned.bounds = bounds;
|
|
499
883
|
}
|
|
500
884
|
|
|
501
885
|
if (node.minWidth) pruned.minWidth = node.minWidth;
|
|
@@ -514,8 +898,9 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
514
898
|
pruned.cornerRadius = node.cornerRadius;
|
|
515
899
|
const radiusToken = resolveVariableToken(bound.cornerRadius, varMap);
|
|
516
900
|
if (radiusToken) pruned.cornerRadiusToken = radiusToken;
|
|
901
|
+
} else if (Array.isArray(node.rectangleCornerRadii) && node.rectangleCornerRadii.some((r) => r > 0)) {
|
|
902
|
+
pruned.cornerRadii = node.rectangleCornerRadii;
|
|
517
903
|
}
|
|
518
|
-
if (node.rectangleCornerRadii) pruned.cornerRadii = node.rectangleCornerRadii;
|
|
519
904
|
if (node.opacity !== undefined && node.opacity !== 1) pruned.opacity = node.opacity;
|
|
520
905
|
|
|
521
906
|
// Effects (Drop Shadows, Layer Blurs)
|
|
@@ -533,25 +918,32 @@ function pruneFigmaNode(node, context = {}, options = {}) {
|
|
|
533
918
|
|
|
534
919
|
// Typography (for TEXT nodes)
|
|
535
920
|
if (node.type === 'TEXT') {
|
|
536
|
-
pruned.characters = node.characters;
|
|
921
|
+
pruned.characters = isRawAssetString(node.characters) ? '[raw-asset-omitted]' : node.characters;
|
|
537
922
|
if (node.style) {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
923
|
+
const typo = {};
|
|
924
|
+
if (node.style.fontFamily) typo.fontFamily = node.style.fontFamily;
|
|
925
|
+
if (node.style.fontSize) typo.fontSize = node.style.fontSize;
|
|
926
|
+
if (node.style.fontWeight) typo.fontWeight = node.style.fontWeight;
|
|
927
|
+
if (node.style.lineHeightPx) typo.lineHeightPx = Math.round(node.style.lineHeightPx);
|
|
928
|
+
if (node.style.letterSpacing) typo.letterSpacing = node.style.letterSpacing;
|
|
929
|
+
if (node.style.textAlignHorizontal && node.style.textAlignHorizontal !== 'LEFT') {
|
|
930
|
+
typo.textAlignHorizontal = node.style.textAlignHorizontal;
|
|
931
|
+
}
|
|
932
|
+
if (node.style.textAlignVertical && node.style.textAlignVertical !== 'TOP') {
|
|
933
|
+
typo.textAlignVertical = node.style.textAlignVertical;
|
|
934
|
+
}
|
|
935
|
+
if (Object.keys(typo).length > 0) {
|
|
936
|
+
pruned.typography = typo;
|
|
937
|
+
}
|
|
547
938
|
}
|
|
548
939
|
}
|
|
549
940
|
|
|
550
941
|
// Recursively process children
|
|
551
942
|
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
.map((c) => pruneFigmaNode(c, context, options));
|
|
943
|
+
const visibleChildren = node.children.filter((c) => c && c.visible !== false);
|
|
944
|
+
if (visibleChildren.length > 0) {
|
|
945
|
+
pruned.children = visibleChildren.map((c) => pruneFigmaNode(c, context, options, _depth + 1));
|
|
946
|
+
}
|
|
555
947
|
}
|
|
556
948
|
|
|
557
949
|
return pruned;
|
|
@@ -566,7 +958,7 @@ const MAX_REQUESTS_PER_WINDOW = 5; // Max 5 calls per minute (well below Figma 1
|
|
|
566
958
|
const MIN_REQUEST_INTERVAL_MS = 2500; // Minimum 2.5s between consecutive requests
|
|
567
959
|
|
|
568
960
|
function getRateGateFilePath() {
|
|
569
|
-
const home =
|
|
961
|
+
const home = resolveHomeDir();
|
|
570
962
|
return home ? path.join(home, '.figma_ratelimit.json') : path.join(process.cwd(), '.figma_ratelimit.json');
|
|
571
963
|
}
|
|
572
964
|
|
|
@@ -1132,15 +1524,24 @@ async function main() {
|
|
|
1132
1524
|
process.exit(1);
|
|
1133
1525
|
}
|
|
1134
1526
|
const { dictionary } = buildVariableMap(varsMeta);
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
config.pretty ? 2 : 0,
|
|
1142
|
-
),
|
|
1527
|
+
const tokenPayload = JSON.stringify(
|
|
1528
|
+
{
|
|
1529
|
+
tokens: dictionary,
|
|
1530
|
+
},
|
|
1531
|
+
null,
|
|
1532
|
+
config.pretty ? 2 : 0,
|
|
1143
1533
|
);
|
|
1534
|
+
if (config.specJsonPath) {
|
|
1535
|
+
const jsonDir = path.dirname(path.resolve(config.specJsonPath));
|
|
1536
|
+
if (!fs.existsSync(jsonDir)) {
|
|
1537
|
+
fs.mkdirSync(jsonDir, { recursive: true });
|
|
1538
|
+
}
|
|
1539
|
+
fs.writeFileSync(path.resolve(config.specJsonPath), tokenPayload, 'utf8');
|
|
1540
|
+
const sizeKb = (Buffer.byteLength(tokenPayload, 'utf8') / 1024).toFixed(1);
|
|
1541
|
+
console.log(`\n✔ Saved Figma design tokens to ${config.specJsonPath} (${sizeKb} KB)\n`);
|
|
1542
|
+
} else {
|
|
1543
|
+
console.log(tokenPayload);
|
|
1544
|
+
}
|
|
1144
1545
|
return;
|
|
1145
1546
|
}
|
|
1146
1547
|
|
|
@@ -1236,7 +1637,7 @@ function formatApiError(error) {
|
|
|
1236
1637
|
} else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
1237
1638
|
isTokenExpiredOrInvalid = true;
|
|
1238
1639
|
errorMessage =
|
|
1239
|
-
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and
|
|
1640
|
+
'FIGMA_TOKEN_EXPIRED_OR_INVALID: The provided Figma access token is expired, invalid, or unauthorized. Create a new token in Figma Settings -> Personal Access Tokens and set FIGMA_ACCESS_TOKEN in your environment or workspace .env file.';
|
|
1240
1641
|
} else if (error.statusCode === 429) {
|
|
1241
1642
|
isRateLimited = true;
|
|
1242
1643
|
let retryMsg = ' Please wait 30-60 seconds before making more requests.';
|
|
@@ -1284,6 +1685,9 @@ function formatApiError(error) {
|
|
|
1284
1685
|
|
|
1285
1686
|
// Export internal functions for unit testing
|
|
1286
1687
|
module.exports = {
|
|
1688
|
+
resolveHomeDir,
|
|
1689
|
+
decodeEnvBuffer,
|
|
1690
|
+
getPowerShellProfilePaths,
|
|
1287
1691
|
loadEnv,
|
|
1288
1692
|
parseEnvLine,
|
|
1289
1693
|
getFigmaToken,
|
|
@@ -67,7 +67,7 @@ When invoked, the agent systematically executes four phases:
|
|
|
67
67
|
|
|
68
68
|
## How to Use the Skill
|
|
69
69
|
|
|
70
|
-
Simply prompt your AI coding assistant (Claude Code, Cursor, OpenCode, Codex, or Antigravity):
|
|
70
|
+
Simply prompt your AI coding assistant (Claude Code, Kiro, Cursor, OpenCode, Codex, or Antigravity):
|
|
71
71
|
|
|
72
72
|
```text
|
|
73
73
|
"Build the component from .specs/transaction-card.json using .specs/transaction-card.png as visual reference"
|