create-vela-workflow 1.0.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 +136 -0
- package/bin/cli.js +188 -0
- package/docs/ai-workflow-tutorial.md +462 -0
- package/docs/official-site-tutorial.md +391 -0
- package/package.json +34 -0
- package/templates/.github/HARNESS-ENGINEERING-GUIDE.md +407 -0
- package/templates/.github/agents/vela-knowledge.agent.md +45 -0
- package/templates/.github/agents/vela-s1-prd.agent.md +69 -0
- package/templates/.github/agents/vela-s2-tech.agent.md +66 -0
- package/templates/.github/agents/vela-s3-coding.agent.md +301 -0
- package/templates/.github/agents/vela-workflow.agent.md +110 -0
- package/templates/.github/copilot-instructions.md +64 -0
- package/templates/.github/prompts/vela-apis.prompt.md +98 -0
- package/templates/.github/prompts/vela-best-practices.prompt.md +93 -0
- package/templates/.github/prompts/vela-components.prompt.md +118 -0
- package/templates/.github/prompts/vela-dev-guide.prompt.md +622 -0
- package/templates/.github/rules/project-init.md +45 -0
- package/templates/.github/rules/vela-coding-convention.md +324 -0
- package/templates/.github/rules/vela-css.md +217 -0
- package/templates/.github/rules/vela-design-driven.md +306 -0
- package/templates/.github/rules/vela-figma-mcp.md +198 -0
- package/templates/.github/rules/vela-format.md +119 -0
- package/templates/.github/rules/vela-layout.md +67 -0
- package/templates/.github/rules/vela-platform.md +46 -0
- package/templates/.github/rules/vela-quality.md +109 -0
- package/templates/.kiro/hooks/figma-design-check.kiro.hook +14 -0
- package/templates/.kiro/hooks/post-coding-validation.kiro.hook +13 -0
- package/templates/.kiro/hooks/validate-ux-files.kiro.hook +16 -0
- package/templates/.kiro/settings/mcp.json +7 -0
- package/templates/.kiro/skills/vela-js-app/SKILL.md +1072 -0
- package/templates/.kiro/steering/workflow-conventions.md +110 -0
- package/templates/.workflow/resource-paths.json +62 -0
- package/templates/.workflow/scripts/.gitkeep +0 -0
- package/templates/.workflow/scripts/checkpoint_manager.js +284 -0
- package/templates/.workflow/scripts/context_loader.js +841 -0
- package/templates/.workflow/scripts/figma_export.js +346 -0
- package/templates/.workflow/scripts/session_manager.js +438 -0
- package/templates/.workflow/stages/.gitkeep +0 -0
- package/templates/.workflow/stages/commands.md +171 -0
- package/templates/.workflow/stages/s1_prd.md +286 -0
- package/templates/.workflow/stages/s2_tech_design.md +302 -0
- package/templates/.workflow/stages/s3_coding.md +699 -0
- package/templates/.workflow/stages/s4_simulator.md +259 -0
- package/templates/.workflow/workflow-config.json +46 -0
- package/templates/.workflow/workflow_starter.md +912 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Figma 导出模块
|
|
3
|
+
*
|
|
4
|
+
* 负责解析 Figma URL、校验 API Token、准备导出请求和保存导出数据。
|
|
5
|
+
* 实际的 Figma API 调用由 Figma MCP 工具在运行时完成,本模块负责
|
|
6
|
+
* URL 解析、Token 校验、目录准备和数据持久化。
|
|
7
|
+
*
|
|
8
|
+
* Requirements: 9.1, 9.2, 9.3, 9.4
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Session 存储根目录
|
|
15
|
+
const SESSIONS_DIR = path.resolve(__dirname, '../../.ai-workspace/sessions');
|
|
16
|
+
|
|
17
|
+
// 用户配置文件路径
|
|
18
|
+
const USER_CONFIG_PATH = path.resolve(__dirname, '../../.ai-workspace/user-config.json');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 解析 Figma 文件 URL,提取 fileKey 和可选的 nodeId
|
|
22
|
+
*
|
|
23
|
+
* 支持的 URL 格式:
|
|
24
|
+
* - https://www.figma.com/file/{fileKey}/...
|
|
25
|
+
* - https://www.figma.com/design/{fileKey}/...
|
|
26
|
+
* - 带 node-id 查询参数: ?node-id=1234:5678 或 ?node-id=1234-5678
|
|
27
|
+
*
|
|
28
|
+
* @param {string} url - Figma 文件 URL
|
|
29
|
+
* @returns {{ success: boolean, fileKey?: string, nodeId?: string, error?: string }}
|
|
30
|
+
*/
|
|
31
|
+
function parseFigmaUrl(url) {
|
|
32
|
+
if (!url || typeof url !== 'string' || url.trim() === '') {
|
|
33
|
+
return { success: false, error: 'Figma URL 不能为空' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const trimmedUrl = url.trim();
|
|
37
|
+
|
|
38
|
+
// 匹配 figma.com/file/{fileKey}、figma.com/design/{fileKey} 或 figma.com/board/{fileKey}
|
|
39
|
+
const pattern = /^https?:\/\/(?:www\.)?figma\.com\/(?:file|design|board)\/([a-zA-Z0-9]+)/;
|
|
40
|
+
const match = trimmedUrl.match(pattern);
|
|
41
|
+
|
|
42
|
+
if (!match) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
error: 'Figma URL 格式无效。期望格式: https://www.figma.com/file/{fileKey}/... 或 https://www.figma.com/design/{fileKey}/...'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fileKey = match[1];
|
|
50
|
+
const result = { success: true, fileKey };
|
|
51
|
+
|
|
52
|
+
// 尝试提取 node-id 查询参数
|
|
53
|
+
try {
|
|
54
|
+
const urlObj = new URL(trimmedUrl);
|
|
55
|
+
const nodeIdParam = urlObj.searchParams.get('node-id');
|
|
56
|
+
if (nodeIdParam) {
|
|
57
|
+
// node-id 可能是 "1234:5678" 或 "1234-5678" 格式
|
|
58
|
+
result.nodeId = nodeIdParam.replace(/-/g, ':');
|
|
59
|
+
}
|
|
60
|
+
} catch (_) {
|
|
61
|
+
// URL 解析失败时忽略 node-id,fileKey 已成功提取
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated 已废弃。Figma 集成现在完全通过 MCP 工具实现,不再需要单独的 API Token。
|
|
69
|
+
* 保留此函数仅为向后兼容,始终返回成功。
|
|
70
|
+
*
|
|
71
|
+
* @returns {{ success: boolean, token?: string }}
|
|
72
|
+
*/
|
|
73
|
+
function loadFigmaToken() {
|
|
74
|
+
return { success: true, token: '' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 导出设计稿(准备阶段)
|
|
80
|
+
*
|
|
81
|
+
* 校验 Figma URL 格式和 API Token,创建 figma-exports/ 目录,
|
|
82
|
+
* 返回解析后的 fileKey 和 nodeId 供 Figma MCP 工具使用。
|
|
83
|
+
*
|
|
84
|
+
* 注意:实际的 Figma API 调用由 MCP 工具在运行时完成,
|
|
85
|
+
* 本函数仅负责校验和准备工作。
|
|
86
|
+
*
|
|
87
|
+
* @param {string} figmaUrl - Figma 文件 URL
|
|
88
|
+
* @param {string} sessionId - Session ID
|
|
89
|
+
* @returns {{ success: boolean, fileKey?: string, nodeId?: string, exportDir?: string, error?: string }}
|
|
90
|
+
*/
|
|
91
|
+
function exportDesign(figmaUrl, sessionId) {
|
|
92
|
+
// 参数校验
|
|
93
|
+
if (!sessionId || typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
94
|
+
return { success: false, error: 'Session ID 不能为空' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 1. 解析 Figma URL
|
|
98
|
+
const urlResult = parseFigmaUrl(figmaUrl);
|
|
99
|
+
if (!urlResult.success) {
|
|
100
|
+
return { success: false, error: urlResult.error };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 2. 校验 Session 目录存在
|
|
104
|
+
const sessionDir = path.join(SESSIONS_DIR, sessionId);
|
|
105
|
+
if (!fs.existsSync(sessionDir)) {
|
|
106
|
+
return { success: false, error: `Session 目录不存在: ${sessionId}。请先创建 Session` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. 创建 figma-exports/ 目录
|
|
110
|
+
const exportDir = path.join(sessionDir, 'figma-exports');
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return { success: false, error: `创建 figma-exports 目录失败: ${err.message}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const result = {
|
|
118
|
+
success: true,
|
|
119
|
+
fileKey: urlResult.fileKey,
|
|
120
|
+
exportDir
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (urlResult.nodeId) {
|
|
124
|
+
result.nodeId = urlResult.nodeId;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 保存 Figma 导出数据
|
|
132
|
+
*
|
|
133
|
+
* 将 Figma MCP 工具返回的 JSON 数据和图片保存到 Session 的 figma-exports/ 目录。
|
|
134
|
+
* - JSON 数据保存为 design_{index}.json(支持多个设计稿)
|
|
135
|
+
* - 图片保存到 images/ 子目录
|
|
136
|
+
*
|
|
137
|
+
* @param {string} sessionId - Session ID
|
|
138
|
+
* @param {object} jsonData - Figma 导出的 JSON 结构数据
|
|
139
|
+
* @param {Array<{name: string, data: Buffer|string}>} [images] - 图片数据数组(可选)
|
|
140
|
+
* @param {number} [index=0] - 设计稿序号(支持多个 Figma 链接时区分不同设计稿)
|
|
141
|
+
* @returns {{ success: boolean, savedFiles?: { json: string, images: string[] }, error?: string }}
|
|
142
|
+
*/
|
|
143
|
+
function saveFigmaData(sessionId, jsonData, images, index) {
|
|
144
|
+
// 参数校验
|
|
145
|
+
if (!sessionId || typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
146
|
+
return { success: false, error: 'Session ID 不能为空' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!jsonData || typeof jsonData !== 'object') {
|
|
150
|
+
return { success: false, error: '导出的 JSON 数据不能为空' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const figmaIndex = (typeof index === 'number' && index >= 0) ? index : 0;
|
|
154
|
+
|
|
155
|
+
const sessionDir = path.join(SESSIONS_DIR, sessionId);
|
|
156
|
+
if (!fs.existsSync(sessionDir)) {
|
|
157
|
+
return { success: false, error: `Session 目录不存在: ${sessionId}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const exportDir = path.join(sessionDir, 'figma-exports');
|
|
161
|
+
const savedFiles = { json: '', images: [] };
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// 确保 figma-exports/ 目录存在
|
|
165
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
166
|
+
|
|
167
|
+
// 保存 JSON 数据(按序号命名:design_0.json, design_1.json, ...)
|
|
168
|
+
const jsonFileName = figmaIndex === 0 ? 'design.json' : `design_${figmaIndex}.json`;
|
|
169
|
+
const jsonPath = path.join(exportDir, jsonFileName);
|
|
170
|
+
const serialized = JSON.stringify(jsonData, null, 2);
|
|
171
|
+
// 验证往返一致性
|
|
172
|
+
try {
|
|
173
|
+
JSON.parse(serialized);
|
|
174
|
+
} catch (validateErr) {
|
|
175
|
+
return { success: false, error: `Figma JSON 数据序列化验证失败: ${validateErr.message}` };
|
|
176
|
+
}
|
|
177
|
+
fs.writeFileSync(jsonPath, serialized, 'utf-8');
|
|
178
|
+
savedFiles.json = jsonPath;
|
|
179
|
+
|
|
180
|
+
// 保存图片(如果有)
|
|
181
|
+
if (images && Array.isArray(images) && images.length > 0) {
|
|
182
|
+
const imagesDir = path.join(exportDir, 'images');
|
|
183
|
+
fs.mkdirSync(imagesDir, { recursive: true });
|
|
184
|
+
|
|
185
|
+
for (const img of images) {
|
|
186
|
+
if (!img || !img.name || !img.data) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const imgPath = path.join(imagesDir, img.name);
|
|
190
|
+
fs.writeFileSync(imgPath, img.data);
|
|
191
|
+
savedFiles.images.push(imgPath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { success: true, savedFiles };
|
|
196
|
+
} catch (err) {
|
|
197
|
+
return { success: false, error: `保存 Figma 导出数据失败: ${err.message}` };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 从 Figma 节点树中提取需要下载的图片/图标节点
|
|
203
|
+
*
|
|
204
|
+
* 扫描 get_figma_data 返回的节点树,识别以下类型的可导出节点:
|
|
205
|
+
* - 包含 imageRef 填充的节点(位图图片)
|
|
206
|
+
* - 类型为 VECTOR/BOOLEAN_OPERATION/LINE/STAR/POLYGON 的矢量图标
|
|
207
|
+
* - 名称包含 "icon"/"ic_"/"img"/"image"/"logo" 的节点
|
|
208
|
+
* - 已配置 exportSettings 的节点
|
|
209
|
+
*
|
|
210
|
+
* @param {object} figmaData - mcp_figma_get_figma_data 返回的节点树数据
|
|
211
|
+
* @param {string} fileKey - Figma 文件 key
|
|
212
|
+
* @returns {{ nodes: Array<{nodeId: string, fileName: string, imageRef?: string}>, fileKey: string }}
|
|
213
|
+
*/
|
|
214
|
+
function extractImageNodes(figmaData, fileKey) {
|
|
215
|
+
const nodes = [];
|
|
216
|
+
const seenIds = new Set();
|
|
217
|
+
|
|
218
|
+
// 图标/图片相关名称模式(支持下划线/连字符分隔和驼峰命名)
|
|
219
|
+
const imageNamePattern = /(?:^|[\s_\-/.])(icon|ic|img|image|logo|illustration|photo|avatar|badge|thumbnail)(?:$|[\s_\-/.])|^(icon|ic_|img|image|logo)/i;
|
|
220
|
+
// 矢量节点类型
|
|
221
|
+
const vectorTypes = new Set(['VECTOR', 'BOOLEAN_OPERATION', 'LINE', 'STAR', 'POLYGON']);
|
|
222
|
+
|
|
223
|
+
function walk(node) {
|
|
224
|
+
if (!node || seenIds.has(node.id)) return;
|
|
225
|
+
|
|
226
|
+
let shouldExport = false;
|
|
227
|
+
let imageRef = null;
|
|
228
|
+
let format = 'png';
|
|
229
|
+
|
|
230
|
+
// 检查是否有 imageRef 填充(位图)
|
|
231
|
+
if (node.fills && Array.isArray(node.fills)) {
|
|
232
|
+
for (const fill of node.fills) {
|
|
233
|
+
if (fill.type === 'IMAGE' && fill.imageRef) {
|
|
234
|
+
shouldExport = true;
|
|
235
|
+
imageRef = fill.imageRef;
|
|
236
|
+
format = 'png';
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 检查是否有 exportSettings
|
|
243
|
+
if (!shouldExport && node.exportSettings && node.exportSettings.length > 0) {
|
|
244
|
+
shouldExport = true;
|
|
245
|
+
const firstSetting = node.exportSettings[0];
|
|
246
|
+
format = (firstSetting.format || 'png').toLowerCase();
|
|
247
|
+
if (format !== 'svg' && format !== 'png') format = 'png';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 检查矢量类型节点(导出为 SVG)
|
|
251
|
+
if (!shouldExport && vectorTypes.has(node.type)) {
|
|
252
|
+
shouldExport = true;
|
|
253
|
+
format = 'svg';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 检查名称匹配
|
|
257
|
+
if (!shouldExport && node.name && imageNamePattern.test(node.name)) {
|
|
258
|
+
shouldExport = true;
|
|
259
|
+
format = vectorTypes.has(node.type) ? 'svg' : 'png';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (shouldExport && node.id) {
|
|
263
|
+
seenIds.add(node.id);
|
|
264
|
+
// 生成安全文件名:将特殊字符替换为下划线
|
|
265
|
+
const safeName = (node.name || `node_${node.id}`)
|
|
266
|
+
.replace(/[^a-zA-Z0-9_\-\u4e00-\u9fff]/g, '_')
|
|
267
|
+
.replace(/_+/g, '_')
|
|
268
|
+
.substring(0, 80);
|
|
269
|
+
const fileName = `${safeName}.${format}`;
|
|
270
|
+
|
|
271
|
+
const entry = { nodeId: node.id.replace(/:/g, ':'), fileName };
|
|
272
|
+
if (imageRef) entry.imageRef = imageRef;
|
|
273
|
+
nodes.push(entry);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 递归子节点
|
|
277
|
+
if (node.children && Array.isArray(node.children)) {
|
|
278
|
+
for (const child of node.children) {
|
|
279
|
+
walk(child);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 处理 figmaData 的不同结构
|
|
285
|
+
if (figmaData.document) {
|
|
286
|
+
walk(figmaData.document);
|
|
287
|
+
} else if (figmaData.nodes) {
|
|
288
|
+
for (const key of Object.keys(figmaData.nodes)) {
|
|
289
|
+
const nodeData = figmaData.nodes[key];
|
|
290
|
+
walk(nodeData.document || nodeData);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
walk(figmaData);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { nodes, fileKey };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 保存通过 mcp_figma_download_figma_images 下载的图片文件路径清单
|
|
301
|
+
*
|
|
302
|
+
* 在 figma-exports/ 目录下生成 image-manifest.json,记录所有已下载图片的信息,
|
|
303
|
+
* 供后续 S3 编码阶段引用。
|
|
304
|
+
*
|
|
305
|
+
* @param {string} sessionId - Session ID
|
|
306
|
+
* @param {Array<{nodeId: string, fileName: string, localPath: string}>} downloadedImages - 已下载的图片列表
|
|
307
|
+
* @returns {{ success: boolean, manifestPath?: string, error?: string }}
|
|
308
|
+
*/
|
|
309
|
+
function saveImageManifest(sessionId, downloadedImages) {
|
|
310
|
+
if (!sessionId || typeof sessionId !== 'string' || sessionId.trim() === '') {
|
|
311
|
+
return { success: false, error: 'Session ID 不能为空' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sessionDir = path.join(SESSIONS_DIR, sessionId);
|
|
315
|
+
if (!fs.existsSync(sessionDir)) {
|
|
316
|
+
return { success: false, error: `Session 目录不存在: ${sessionId}` };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const exportDir = path.join(sessionDir, 'figma-exports');
|
|
320
|
+
fs.mkdirSync(exportDir, { recursive: true });
|
|
321
|
+
|
|
322
|
+
const manifestPath = path.join(exportDir, 'image-manifest.json');
|
|
323
|
+
const manifest = {
|
|
324
|
+
generated_at: new Date().toISOString(),
|
|
325
|
+
image_count: downloadedImages.length,
|
|
326
|
+
images: downloadedImages
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
331
|
+
return { success: true, manifestPath };
|
|
332
|
+
} catch (err) {
|
|
333
|
+
return { success: false, error: `保存图片清单失败: ${err.message}` };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = {
|
|
338
|
+
parseFigmaUrl,
|
|
339
|
+
loadFigmaToken,
|
|
340
|
+
exportDesign,
|
|
341
|
+
saveFigmaData,
|
|
342
|
+
extractImageNodes,
|
|
343
|
+
saveImageManifest,
|
|
344
|
+
SESSIONS_DIR,
|
|
345
|
+
USER_CONFIG_PATH
|
|
346
|
+
};
|