@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;
|
|
161
|
+
}
|
|
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
|
+
}
|
|
50
179
|
}
|
|
51
|
-
|
|
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
|
|
|
@@ -1173,7 +1565,7 @@ function formatApiError(error) {
|
|
|
1173
1565
|
} else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
1174
1566
|
isTokenExpiredOrInvalid = true;
|
|
1175
1567
|
errorMessage =
|
|
1176
|
-
'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
|
|
1568
|
+
'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.';
|
|
1177
1569
|
} else if (error.statusCode === 429) {
|
|
1178
1570
|
isRateLimited = true;
|
|
1179
1571
|
let retryMsg = ' Please wait 30-60 seconds before making more requests.';
|
|
@@ -1221,6 +1613,9 @@ function formatApiError(error) {
|
|
|
1221
1613
|
|
|
1222
1614
|
// Export internal functions for unit testing
|
|
1223
1615
|
module.exports = {
|
|
1616
|
+
resolveHomeDir,
|
|
1617
|
+
decodeEnvBuffer,
|
|
1618
|
+
getPowerShellProfilePaths,
|
|
1224
1619
|
loadEnv,
|
|
1225
1620
|
parseEnvLine,
|
|
1226
1621
|
getFigmaToken,
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pedroab0/skills",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Curated collection of modular, production-ready skills for AI coding agents (Claude Code, Cursor, OpenCode, Antigravity)",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Curated collection of modular, production-ready skills for AI coding agents (Claude Code, Cursor, Kiro, OpenCode, Antigravity)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"skills": "bin/cli.js",
|
|
7
7
|
"skills-installer": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "./bin/cli.js",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=16.7.0"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"test": "node figma-extractor/scripts/test_fetch_figma.js && node figma-to-code/scripts/test_fetch_figma.js && node figma-node-builder/scripts/test_inspect_spec.js && node test/test_cli.js",
|
|
12
15
|
"install-skills": "node ./bin/cli.js"
|
|
@@ -20,6 +23,7 @@
|
|
|
20
23
|
"skills",
|
|
21
24
|
"claude-code",
|
|
22
25
|
"cursor",
|
|
26
|
+
"kiro",
|
|
23
27
|
"antigravity",
|
|
24
28
|
"opencode",
|
|
25
29
|
"figma",
|
|
@@ -37,6 +41,11 @@
|
|
|
37
41
|
"figma-extractor",
|
|
38
42
|
"figma-node-builder",
|
|
39
43
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"!**/demo/**",
|
|
46
|
+
"!**/demos/**",
|
|
47
|
+
"!**/.specs/**",
|
|
48
|
+
"!**/test_*.js",
|
|
49
|
+
"!**/figma-plugin/assets/**"
|
|
41
50
|
]
|
|
42
51
|
}
|