@su-record/vibe 2.8.31 → 2.8.33
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/dist/cli/postinstall/fs-utils.d.ts +2 -2
- package/dist/cli/postinstall/fs-utils.d.ts.map +1 -1
- package/dist/cli/postinstall/fs-utils.js +5 -6
- package/dist/cli/postinstall/fs-utils.js.map +1 -1
- package/hooks/scripts/figma-extract.js +225 -0
- package/package.json +1 -1
- package/skills/vibe.figma/SKILL.md +4 -16
- package/skills/vibe.figma.extract/SKILL.md +4 -16
|
@@ -25,9 +25,9 @@ export declare function copyDirRecursive(src: string, dest: string): void;
|
|
|
25
25
|
*/
|
|
26
26
|
export declare function removeDirRecursive(dir: string): void;
|
|
27
27
|
/**
|
|
28
|
-
* 스킬 복사 (
|
|
28
|
+
* 스킬 복사 (항상 덮어쓰기 — 패키지 업데이트 시 최신 버전 반영)
|
|
29
29
|
*/
|
|
30
|
-
export declare function
|
|
30
|
+
export declare function copySkillsOverwrite(src: string, dest: string): void;
|
|
31
31
|
/**
|
|
32
32
|
* 레거시 스킬 디렉토리 삭제 (이름 변경된 구 디렉토리 정리)
|
|
33
33
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-utils.d.ts","sourceRoot":"","sources":["../../../src/cli/postinstall/fs-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAE/E;;GAEG;AACH,eAAO,MAAM,gBAAgB,2BAAqB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkB3D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAKD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAahE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"fs-utils.d.ts","sourceRoot":"","sources":["../../../src/cli/postinstall/fs-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,2CAA2C,CAAC;AAE/E;;GAEG;AACH,eAAO,MAAM,gBAAgB,2BAAqB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAkB3D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI3C;AAKD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAahE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAIpD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAYnE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,GAChC,IAAI,CAON;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,GACnC,IAAI,CAUN"}
|
|
@@ -70,19 +70,18 @@ export function removeDirRecursive(dir) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* 스킬 복사 (
|
|
73
|
+
* 스킬 복사 (항상 덮어쓰기 — 패키지 업데이트 시 최신 버전 반영)
|
|
74
74
|
*/
|
|
75
|
-
export function
|
|
75
|
+
export function copySkillsOverwrite(src, dest) {
|
|
76
76
|
ensureDir(dest);
|
|
77
77
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
78
78
|
for (const entry of entries) {
|
|
79
79
|
const srcPath = path.join(src, entry.name);
|
|
80
80
|
const destPath = path.join(dest, entry.name);
|
|
81
81
|
if (entry.isDirectory()) {
|
|
82
|
-
|
|
82
|
+
copySkillsOverwrite(srcPath, destPath);
|
|
83
83
|
}
|
|
84
|
-
else
|
|
85
|
-
// 파일이 없을 때만 복사
|
|
84
|
+
else {
|
|
86
85
|
fs.copyFileSync(srcPath, destPath);
|
|
87
86
|
}
|
|
88
87
|
}
|
|
@@ -114,7 +113,7 @@ export function copySkillsFiltered(src, dest, allowedSkills) {
|
|
|
114
113
|
continue;
|
|
115
114
|
const srcPath = path.join(src, entry.name);
|
|
116
115
|
const destPath = path.join(dest, entry.name);
|
|
117
|
-
|
|
116
|
+
copySkillsOverwrite(srcPath, destPath);
|
|
118
117
|
}
|
|
119
118
|
}
|
|
120
119
|
//# sourceMappingURL=fs-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs-utils.js","sourceRoot":"","sources":["../../../src/cli/postinstall/fs-utils.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"fs-utils.js","sourceRoot":"","sources":["../../../src/cli/postinstall/fs-utils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* figma-extract.js — Figma REST API 디자인 추출 도구
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node figma-extract.js tree <fileKey> <nodeId> [--depth=10]
|
|
8
|
+
* node figma-extract.js images <fileKey> <nodeId> --out=<dir> [--depth=10]
|
|
9
|
+
* node figma-extract.js screenshot <fileKey> <nodeId> --out=<path>
|
|
10
|
+
*
|
|
11
|
+
* Token: ~/.vibe/config.json → credentials.figma.accessToken
|
|
12
|
+
* 또는 FIGMA_ACCESS_TOKEN env
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import os from 'os';
|
|
18
|
+
|
|
19
|
+
// ─── Config ─────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const FIGMA_API = 'https://api.figma.com/v1';
|
|
22
|
+
const MAX_RETRIES = 3;
|
|
23
|
+
const INITIAL_DELAY_MS = 2000;
|
|
24
|
+
|
|
25
|
+
function loadToken() {
|
|
26
|
+
if (process.env.FIGMA_ACCESS_TOKEN) return process.env.FIGMA_ACCESS_TOKEN;
|
|
27
|
+
const configPath = path.join(os.homedir(), '.vibe', 'config.json');
|
|
28
|
+
try {
|
|
29
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
30
|
+
const token = config?.credentials?.figma?.accessToken;
|
|
31
|
+
if (token) return token;
|
|
32
|
+
} catch { /* ignore */ }
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fail(msg) { console.error(JSON.stringify({ error: msg })); process.exit(1); }
|
|
37
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
38
|
+
|
|
39
|
+
// ─── HTTP ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
async function apiFetch(endpoint, token) {
|
|
42
|
+
let lastErr = '';
|
|
43
|
+
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(`${FIGMA_API}${endpoint}`, { headers: { 'X-Figma-Token': token } });
|
|
46
|
+
if (res.status === 429) { await sleep(INITIAL_DELAY_MS * 2 ** i); continue; }
|
|
47
|
+
if (res.status === 403) fail('403 Forbidden — check token permissions');
|
|
48
|
+
if (res.status === 404) fail('404 — check fileKey/nodeId');
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
lastErr = `HTTP ${res.status}`;
|
|
51
|
+
if (res.status >= 500) { await sleep(INITIAL_DELAY_MS * 2 ** i); continue; }
|
|
52
|
+
fail(lastErr);
|
|
53
|
+
}
|
|
54
|
+
return await res.json();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
lastErr = e.message;
|
|
57
|
+
if (i < MAX_RETRIES - 1) await sleep(INITIAL_DELAY_MS * 2 ** i);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
fail(`Failed after ${MAX_RETRIES} retries: ${lastErr}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Color ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function toCSS(c) {
|
|
66
|
+
if (!c) return null;
|
|
67
|
+
const r = Math.round(c.r * 255), g = Math.round(c.g * 255), b = Math.round(c.b * 255), a = c.a ?? 1;
|
|
68
|
+
if (a === 1) return `#${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`;
|
|
69
|
+
return `rgba(${r}, ${g}, ${b}, ${+a.toFixed(2)})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── CSS Extraction ─────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function extractCSS(n) {
|
|
75
|
+
const css = {};
|
|
76
|
+
// Layout
|
|
77
|
+
if (n.layoutMode === 'VERTICAL') { css.display = 'flex'; css.flexDirection = 'column'; }
|
|
78
|
+
else if (n.layoutMode === 'HORIZONTAL') { css.display = 'flex'; css.flexDirection = 'row'; }
|
|
79
|
+
const axM = { MIN:'flex-start', CENTER:'center', MAX:'flex-end', SPACE_BETWEEN:'space-between' };
|
|
80
|
+
const crM = { MIN:'flex-start', CENTER:'center', MAX:'flex-end', BASELINE:'baseline' };
|
|
81
|
+
if (n.primaryAxisAlignItems && axM[n.primaryAxisAlignItems]) css.justifyContent = axM[n.primaryAxisAlignItems];
|
|
82
|
+
if (n.counterAxisAlignItems && crM[n.counterAxisAlignItems]) css.alignItems = crM[n.counterAxisAlignItems];
|
|
83
|
+
if (n.itemSpacing > 0) css.gap = `${n.itemSpacing}px`;
|
|
84
|
+
// Padding
|
|
85
|
+
const pt=n.paddingTop||0, pr=n.paddingRight||0, pb=n.paddingBottom||0, pl=n.paddingLeft||0;
|
|
86
|
+
if (pt||pr||pb||pl) css.padding = `${pt}px ${pr}px ${pb}px ${pl}px`;
|
|
87
|
+
// Size
|
|
88
|
+
if (n.absoluteBoundingBox) { css.width = `${Math.round(n.absoluteBoundingBox.width)}px`; css.height = `${Math.round(n.absoluteBoundingBox.height)}px`; }
|
|
89
|
+
// Position / overflow / opacity
|
|
90
|
+
if (n.layoutPositioning === 'ABSOLUTE') css.position = 'absolute';
|
|
91
|
+
if (n.clipsContent) css.overflow = 'hidden';
|
|
92
|
+
if (n.opacity != null && n.opacity < 1) css.opacity = n.opacity.toFixed(2);
|
|
93
|
+
// Blend
|
|
94
|
+
const bm = { MULTIPLY:'multiply', SCREEN:'screen', OVERLAY:'overlay', DARKEN:'darken', LIGHTEN:'lighten', COLOR_DODGE:'color-dodge', COLOR_BURN:'color-burn', HARD_LIGHT:'hard-light', SOFT_LIGHT:'soft-light', DIFFERENCE:'difference', EXCLUSION:'exclusion', HUE:'hue', SATURATION:'saturation', COLOR:'color', LUMINOSITY:'luminosity' };
|
|
95
|
+
if (n.blendMode && bm[n.blendMode]) css.mixBlendMode = bm[n.blendMode];
|
|
96
|
+
// Radius
|
|
97
|
+
if (n.cornerRadius > 0) css.borderRadius = `${n.cornerRadius}px`;
|
|
98
|
+
else if (n.rectangleCornerRadii) { const [a,b,c,d] = n.rectangleCornerRadii; css.borderRadius = `${a}px ${b}px ${c}px ${d}px`; }
|
|
99
|
+
// Fills
|
|
100
|
+
let imgRef;
|
|
101
|
+
for (const f of (n.fills||[]).filter(f=>f.visible!==false)) {
|
|
102
|
+
if (f.type === 'SOLID') css.backgroundColor = toCSS({ ...f.color, a: f.opacity ?? f.color?.a ?? 1 });
|
|
103
|
+
else if (f.type === 'IMAGE') imgRef = f.imageRef;
|
|
104
|
+
}
|
|
105
|
+
// Strokes
|
|
106
|
+
const stroke = (n.strokes||[]).find(s=>s.visible!==false&&s.type==='SOLID');
|
|
107
|
+
if (stroke && n.strokeWeight) css.border = `${n.strokeWeight}px solid ${toCSS(stroke.color)}`;
|
|
108
|
+
// Effects
|
|
109
|
+
const shadows = [];
|
|
110
|
+
for (const e of (n.effects||[]).filter(e=>e.visible!==false)) {
|
|
111
|
+
if (e.type==='DROP_SHADOW'||e.type==='INNER_SHADOW') {
|
|
112
|
+
const ins = e.type==='INNER_SHADOW'?'inset ':'';
|
|
113
|
+
shadows.push(`${ins}${e.offset?.x||0}px ${e.offset?.y||0}px ${e.radius||0}px ${e.spread||0}px ${toCSS(e.color)}`);
|
|
114
|
+
} else if (e.type==='LAYER_BLUR') css.filter = `blur(${e.radius}px)`;
|
|
115
|
+
else if (e.type==='BACKGROUND_BLUR') css.backdropFilter = `blur(${e.radius}px)`;
|
|
116
|
+
}
|
|
117
|
+
if (shadows.length) css.boxShadow = shadows.join(', ');
|
|
118
|
+
// Text
|
|
119
|
+
if (n.type === 'TEXT' && n.style) {
|
|
120
|
+
const s = n.style;
|
|
121
|
+
if (s.fontFamily) css.fontFamily = `'${s.fontFamily}', sans-serif`;
|
|
122
|
+
if (s.fontSize) css.fontSize = `${s.fontSize}px`;
|
|
123
|
+
if (s.fontWeight) css.fontWeight = String(s.fontWeight);
|
|
124
|
+
if (s.lineHeightPx) css.lineHeight = `${s.lineHeightPx}px`;
|
|
125
|
+
if (s.letterSpacing) css.letterSpacing = `${s.letterSpacing}px`;
|
|
126
|
+
const ta = { LEFT:'left', CENTER:'center', RIGHT:'right', JUSTIFIED:'justify' };
|
|
127
|
+
if (s.textAlignHorizontal && ta[s.textAlignHorizontal]) css.textAlign = ta[s.textAlignHorizontal];
|
|
128
|
+
const tf = (n.fills||[]).find(f=>f.visible!==false&&f.type==='SOLID');
|
|
129
|
+
if (tf) css.color = toCSS(tf.color);
|
|
130
|
+
}
|
|
131
|
+
return imgRef ? { ...css, _imageRef: imgRef } : css;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Tree ───────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function walk(node) {
|
|
137
|
+
const css = extractCSS(node);
|
|
138
|
+
const r = { nodeId: node.id, name: node.name||'', type: node.type, size: null, css: {...css}, children: [] };
|
|
139
|
+
if (node.type==='TEXT' && node.characters) r.text = node.characters;
|
|
140
|
+
if (node.absoluteBoundingBox) r.size = { width: Math.round(node.absoluteBoundingBox.width), height: Math.round(node.absoluteBoundingBox.height) };
|
|
141
|
+
if (css._imageRef) { r.imageRef = css._imageRef; delete r.css._imageRef; }
|
|
142
|
+
if (node.children?.length) r.children = node.children.map(walk);
|
|
143
|
+
return r;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function collectRefs(node, set = new Set()) {
|
|
147
|
+
if (node.imageRef) set.add(node.imageRef);
|
|
148
|
+
(node.children||[]).forEach(c => collectRefs(c, set));
|
|
149
|
+
return set;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── Commands ───────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
async function cmdTree(token, fk, nid, depth) {
|
|
155
|
+
const dp = depth ? `&depth=${depth}` : '';
|
|
156
|
+
const data = await apiFetch(`/files/${fk}/nodes?ids=${nid}${dp}`, token);
|
|
157
|
+
const nd = data.nodes?.[nid];
|
|
158
|
+
if (!nd?.document) fail(`Node ${nid} not found`);
|
|
159
|
+
console.log(JSON.stringify(walk(nd.document), null, 2));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function cmdImages(token, fk, nid, outDir, depth) {
|
|
163
|
+
if (!outDir) fail('--out required');
|
|
164
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
165
|
+
// tree → refs
|
|
166
|
+
const dp = depth ? `&depth=${depth}` : '';
|
|
167
|
+
const data = await apiFetch(`/files/${fk}/nodes?ids=${nid}${dp}`, token);
|
|
168
|
+
const nd = data.nodes?.[nid];
|
|
169
|
+
if (!nd?.document) fail(`Node ${nid} not found`);
|
|
170
|
+
const tree = walk(nd.document);
|
|
171
|
+
const refs = collectRefs(tree);
|
|
172
|
+
if (!refs.size) { console.log(JSON.stringify({ total: 0, images: {} })); return; }
|
|
173
|
+
// download
|
|
174
|
+
const allImg = await apiFetch(`/files/${fk}/images`, token);
|
|
175
|
+
const urls = allImg.meta?.images || {};
|
|
176
|
+
const imageMap = {};
|
|
177
|
+
const dl = [];
|
|
178
|
+
for (const ref of refs) {
|
|
179
|
+
const url = urls[ref];
|
|
180
|
+
if (!url) continue;
|
|
181
|
+
const out = path.join(outDir, ref.slice(0,16) + '.png');
|
|
182
|
+
dl.push(fetch(url).then(r=>r.arrayBuffer()).then(b=>{
|
|
183
|
+
fs.writeFileSync(out, Buffer.from(b));
|
|
184
|
+
const sz = fs.statSync(out).size;
|
|
185
|
+
if (sz > 0) imageMap[ref] = out;
|
|
186
|
+
}).catch(()=>{}));
|
|
187
|
+
}
|
|
188
|
+
await Promise.all(dl);
|
|
189
|
+
console.log(JSON.stringify({ total: Object.keys(imageMap).length, images: imageMap }, null, 2));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function cmdScreenshot(token, fk, nid, outPath) {
|
|
193
|
+
if (!outPath) fail('--out required');
|
|
194
|
+
const data = await apiFetch(`/images/${fk}?ids=${nid}&format=png&scale=2`, token);
|
|
195
|
+
const url = data.images?.[nid];
|
|
196
|
+
if (!url) fail(`No image for ${nid}`);
|
|
197
|
+
const dir = path.dirname(outPath);
|
|
198
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
199
|
+
const buf = Buffer.from(await (await fetch(url)).arrayBuffer());
|
|
200
|
+
fs.writeFileSync(outPath, buf);
|
|
201
|
+
console.log(JSON.stringify({ path: outPath, size: buf.length }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── CLI ────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
const args = process.argv.slice(2);
|
|
207
|
+
const flags = {};
|
|
208
|
+
const pos = [];
|
|
209
|
+
for (const a of args) {
|
|
210
|
+
if (a.startsWith('--')) { const [k,v] = a.slice(2).split('='); flags[k] = v ?? ''; }
|
|
211
|
+
else pos.push(a);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const token = loadToken();
|
|
215
|
+
if (!token) fail('Figma token not found. Run: vibe figma setup <token>');
|
|
216
|
+
|
|
217
|
+
const [cmd, fk, nidRaw] = pos;
|
|
218
|
+
const nid = nidRaw?.replace(/-/g, ':');
|
|
219
|
+
|
|
220
|
+
switch (cmd) {
|
|
221
|
+
case 'tree': await cmdTree(token, fk, nid, flags.depth ? +flags.depth : undefined); break;
|
|
222
|
+
case 'images': await cmdImages(token, fk, nid, flags.out, flags.depth ? +flags.depth : 10); break;
|
|
223
|
+
case 'screenshot': await cmdScreenshot(token, fk, nid, flags.out); break;
|
|
224
|
+
default: console.log('Usage: node figma-extract.js <tree|images|screenshot> <fileKey> <nodeId> [flags]');
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -235,11 +235,8 @@ Figma REST API로 노드 트리와 CSS 속성을 직접 추출한다.
|
|
|
235
235
|
MCP 플러그인(get_design_context/get_metadata)은 사용하지 않는다.
|
|
236
236
|
|
|
237
237
|
Bash:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const tree = await getTree({ fileKey: '{fileKey}', nodeId: '{섹션.nodeId}', depth: 10 });
|
|
241
|
-
console.log(JSON.stringify(tree));
|
|
242
|
-
"
|
|
238
|
+
# [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
|
|
239
|
+
node "[FIGMA_SCRIPT]" tree {fileKey} {섹션.nodeId} --depth=10
|
|
243
240
|
|
|
244
241
|
반환 (JSON):
|
|
245
242
|
{
|
|
@@ -265,15 +262,7 @@ CSS는 Figma 노드 속성에서 직접 추출 — Tailwind 역변환 불필요:
|
|
|
265
262
|
트리에서 imageRef가 있는 노드를 수집 → Figma API로 다운로드.
|
|
266
263
|
|
|
267
264
|
Bash:
|
|
268
|
-
node
|
|
269
|
-
import { getTree, getImages, collectImageRefs } from './dist/infra/lib/figma/index.js';
|
|
270
|
-
const tree = await getTree({ fileKey: '{fileKey}', nodeId: '{섹션.nodeId}', depth: 10 });
|
|
271
|
-
const refs = collectImageRefs(tree);
|
|
272
|
-
const result = await getImages({
|
|
273
|
-
fileKey: '{fileKey}', imageRefs: refs, outDir: 'images/{feature}/'
|
|
274
|
-
});
|
|
275
|
-
console.log(JSON.stringify(result));
|
|
276
|
-
"
|
|
265
|
+
node "[FIGMA_SCRIPT]" images {fileKey} {섹션.nodeId} --out=images/{feature}/ --depth=10
|
|
277
266
|
|
|
278
267
|
검증: result.total = refs.size (누락 0)
|
|
279
268
|
전부 완료해야 c 단계로 진행.
|
|
@@ -397,8 +386,7 @@ Grep 체크:
|
|
|
397
386
|
|
|
398
387
|
시각 검증:
|
|
399
388
|
각 섹션 스크린샷:
|
|
400
|
-
node
|
|
401
|
-
await getScreenshot({ fileKey: '{fileKey}', nodeId: '{nodeId}', outPath: '/tmp/{section}.png' });"
|
|
389
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out=/tmp/{section}.png
|
|
402
390
|
→ dev 서버/preview와 비교
|
|
403
391
|
P1 (필수): 이미지 누락, 레이아웃 구조 다름, 텍스트 스타일 미적용
|
|
404
392
|
P2 (권장): 미세 간격, 미세 색상 차이
|
|
@@ -15,11 +15,8 @@ Figma REST API(`src/infra/lib/figma/`)를 사용하여 노드 트리, CSS, 이
|
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
Bash:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const tree = await getTree({ fileKey: '{fileKey}', nodeId: '{nodeId}', depth: 10 });
|
|
21
|
-
console.log(JSON.stringify(tree));
|
|
22
|
-
"
|
|
18
|
+
# [FIGMA_SCRIPT] = ~/.vibe/hooks/scripts/figma-extract.js
|
|
19
|
+
node "[FIGMA_SCRIPT]" tree {fileKey} {nodeId} --depth=10
|
|
23
20
|
|
|
24
21
|
반환 (FigmaNode JSON):
|
|
25
22
|
{
|
|
@@ -75,13 +72,7 @@ Bash:
|
|
|
75
72
|
트리에서 imageRef 수집 → Figma API로 다운로드:
|
|
76
73
|
|
|
77
74
|
Bash:
|
|
78
|
-
node
|
|
79
|
-
import { getTree, getImages, collectImageRefs } from './dist/infra/lib/figma/index.js';
|
|
80
|
-
const tree = await getTree({ fileKey: '{fileKey}', nodeId: '{nodeId}', depth: 10 });
|
|
81
|
-
const refs = collectImageRefs(tree);
|
|
82
|
-
const result = await getImages({ fileKey: '{fileKey}', imageRefs: refs, outDir: '{outDir}' });
|
|
83
|
-
console.log(JSON.stringify(result));
|
|
84
|
-
"
|
|
75
|
+
node "[FIGMA_SCRIPT]" images {fileKey} {nodeId} --out={outDir} --depth=10
|
|
85
76
|
|
|
86
77
|
반환: { total: N, images: { "imageRef": "/path/to/file.png", ... } }
|
|
87
78
|
|
|
@@ -95,10 +86,7 @@ Bash:
|
|
|
95
86
|
|
|
96
87
|
```
|
|
97
88
|
Bash:
|
|
98
|
-
node
|
|
99
|
-
import { getScreenshot } from './dist/infra/lib/figma/index.js';
|
|
100
|
-
await getScreenshot({ fileKey: '{fileKey}', nodeId: '{nodeId}', outPath: '{path}' });
|
|
101
|
-
"
|
|
89
|
+
node "[FIGMA_SCRIPT]" screenshot {fileKey} {nodeId} --out={path}
|
|
102
90
|
|
|
103
91
|
시각 검증용. 노드를 PNG로 렌더링하여 저장.
|
|
104
92
|
```
|