figma-cache-toolchain 2.0.3 → 2.0.5
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 +197 -170
- package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +75 -43
- package/cursor-bootstrap/examples/README.md +26 -15
- package/cursor-bootstrap/examples/ui-adapter.contract.template.json +90 -0
- package/cursor-bootstrap/examples/ui-execution-template.fast.md +11 -0
- package/cursor-bootstrap/examples/ui-execution-template.strict.md +13 -0
- package/cursor-bootstrap/examples/ui-override.template.json +26 -0
- package/cursor-bootstrap/figma-cache.config.example.js +51 -9
- package/cursor-bootstrap/managed-files.json +40 -40
- package/cursor-bootstrap/skills/figma-ui-dual-mode-execution/SKILL.md +55 -37
- package/figma-cache/adapters/recipes/button.recipe.json +24 -0
- package/figma-cache/adapters/recipes/card.recipe.json +24 -0
- package/figma-cache/adapters/recipes/checkbox.recipe.json +24 -0
- package/figma-cache/adapters/recipes/input.recipe.json +24 -0
- package/figma-cache/adapters/recipes/modal.recipe.json +25 -0
- package/figma-cache/adapters/recipes/radio.recipe.json +23 -0
- package/figma-cache/adapters/recipes/select.recipe.json +24 -0
- package/figma-cache/adapters/recipes/table.recipe.json +25 -0
- package/figma-cache/adapters/recipes/tabs.recipe.json +24 -0
- package/figma-cache/adapters/recipes/tooltip.recipe.json +24 -0
- package/figma-cache/docs/README.md +323 -237
- package/figma-cache/docs/p0-ui-preflight-handoff.md +207 -0
- package/figma-cache/docs/ui-1to1-optimization-roadmap.md +182 -0
- package/figma-cache/docs/ui-1to1-report.schema.json +104 -0
- package/figma-cache/figma-cache.js +639 -562
- package/figma-cache/js/contract-check-cli.js +466 -0
- package/figma-cache/js/cursor-bootstrap-cli.js +22 -0
- package/figma-cache/js/ui-facts-normalizer.js +233 -0
- package/package.json +93 -73
- package/scripts/cross-project-e2e.js +594 -0
- package/scripts/ui-1to1-audit.js +431 -0
- package/scripts/ui-auto-acceptance.js +248 -0
- package/scripts/ui-preflight.js +289 -0
- package/scripts/ui-profile.js +46 -0
- package/scripts/ui-report-aggregate.js +124 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const ROOT = process.cwd();
|
|
10
|
+
const FAIL_EXIT_CODE = 2;
|
|
11
|
+
|
|
12
|
+
function resolveMaybeAbsolutePath(input) {
|
|
13
|
+
if (!input) {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
return path.isAbsolute(input) ? path.normalize(input) : path.join(ROOT, input);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const options = {
|
|
21
|
+
targetProject: "",
|
|
22
|
+
cacheKey: "",
|
|
23
|
+
fileKey: "",
|
|
24
|
+
nodeId: "",
|
|
25
|
+
target: "",
|
|
26
|
+
minScore: 90,
|
|
27
|
+
maxWarnings: 0,
|
|
28
|
+
maxDiffs: 2,
|
|
29
|
+
profile: "",
|
|
30
|
+
keepPackage: false,
|
|
31
|
+
autoBootstrapContract: true,
|
|
32
|
+
autoEnsureOnMiss: false,
|
|
33
|
+
allowSkeletonWithFigmaMcp: false,
|
|
34
|
+
completeness: "layout,text,tokens,interactions,states,accessibility",
|
|
35
|
+
batchFile: "",
|
|
36
|
+
fixLoop: 0,
|
|
37
|
+
emitAgentTaskOnFail: false,
|
|
38
|
+
agentTaskPath: "",
|
|
39
|
+
allowSkippedCodeLevelComparison: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
argv.forEach((arg) => {
|
|
43
|
+
if (arg.startsWith("--target-project=")) {
|
|
44
|
+
options.targetProject = arg.split("=").slice(1).join("=").trim();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (arg.startsWith("--cacheKey=")) {
|
|
48
|
+
options.cacheKey = arg.split("=").slice(1).join("=").trim();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (arg.startsWith("--fileKey=")) {
|
|
52
|
+
options.fileKey = arg.split("=").slice(1).join("=").trim();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (arg.startsWith("--nodeId=")) {
|
|
56
|
+
options.nodeId = arg.split("=").slice(1).join("=").trim();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (arg.startsWith("--target=")) {
|
|
60
|
+
options.target = arg.split("=").slice(1).join("=").trim();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (arg.startsWith("--min-score=")) {
|
|
64
|
+
const n = Number(arg.split("=").slice(1).join("=").trim());
|
|
65
|
+
options.minScore = Number.isFinite(n) ? n : options.minScore;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (arg.startsWith("--max-warnings=")) {
|
|
69
|
+
const n = Number(arg.split("=").slice(1).join("=").trim());
|
|
70
|
+
options.maxWarnings = Number.isFinite(n) ? n : options.maxWarnings;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (arg.startsWith("--max-diffs=")) {
|
|
74
|
+
const n = Number(arg.split("=").slice(1).join("=").trim());
|
|
75
|
+
options.maxDiffs = Number.isFinite(n) ? n : options.maxDiffs;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (arg.startsWith("--profile=")) {
|
|
79
|
+
options.profile = arg.split("=").slice(1).join("=").trim();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--keep-package") {
|
|
83
|
+
options.keepPackage = true;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "--no-auto-bootstrap-contract") {
|
|
87
|
+
options.autoBootstrapContract = false;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (arg === "--auto-ensure-on-miss") {
|
|
91
|
+
options.autoEnsureOnMiss = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (arg === "--allow-skeleton-with-figma-mcp") {
|
|
95
|
+
options.allowSkeletonWithFigmaMcp = true;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith("--completeness=")) {
|
|
99
|
+
options.completeness = arg.split("=").slice(1).join("=").trim() || options.completeness;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (arg.startsWith("--batch-file=")) {
|
|
103
|
+
options.batchFile = arg.split("=").slice(1).join("=").trim();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (arg.startsWith("--fix-loop=")) {
|
|
107
|
+
const n = Number(arg.split("=").slice(1).join("=").trim());
|
|
108
|
+
options.fixLoop = Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (arg === "--emit-agent-task-on-fail") {
|
|
112
|
+
options.emitAgentTaskOnFail = true;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (arg.startsWith("--agent-task-path=")) {
|
|
116
|
+
options.agentTaskPath = arg.split("=").slice(1).join("=").trim();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (arg === "--allow-skipped-code-level-comparison") {
|
|
120
|
+
options.allowSkippedCodeLevelComparison = true;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return options;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function runCommand(command, cwd, extraEnv) {
|
|
128
|
+
return execSync(command, {
|
|
129
|
+
cwd,
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
132
|
+
env: {
|
|
133
|
+
...process.env,
|
|
134
|
+
...(extraEnv || {}),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeNodeId(input) {
|
|
140
|
+
const value = String(input || "").trim();
|
|
141
|
+
if (!value) {
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
return value.includes(":") ? value : value.replace(/-/g, ":");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveCacheKey(options) {
|
|
148
|
+
if (options.cacheKey) {
|
|
149
|
+
return options.cacheKey;
|
|
150
|
+
}
|
|
151
|
+
if (options.fileKey && options.nodeId) {
|
|
152
|
+
return `${options.fileKey}#${normalizeNodeId(options.nodeId)}`;
|
|
153
|
+
}
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function npmPackAndGetTarball() {
|
|
158
|
+
const raw = runCommand("npm pack", ROOT);
|
|
159
|
+
const lines = raw
|
|
160
|
+
.split(/\r?\n/)
|
|
161
|
+
.map((line) => line.trim())
|
|
162
|
+
.filter(Boolean);
|
|
163
|
+
const fileName = [...lines].reverse().find((line) => /\.tgz$/i.test(line));
|
|
164
|
+
if (!fileName) {
|
|
165
|
+
throw new Error("npm pack returned no tarball name");
|
|
166
|
+
}
|
|
167
|
+
return path.join(ROOT, fileName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readJsonOrNull(absPath) {
|
|
171
|
+
try {
|
|
172
|
+
return JSON.parse(fs.readFileSync(absPath, "utf8"));
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeSlash(input) {
|
|
179
|
+
return String(input || "").replace(/\\/g, "/");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeAgentTask(targetProject, options, payload) {
|
|
183
|
+
const defaultPath = path.join(targetProject, "agent-task.md");
|
|
184
|
+
const taskPath = options.agentTaskPath
|
|
185
|
+
? resolveMaybeAbsolutePath(options.agentTaskPath)
|
|
186
|
+
: defaultPath;
|
|
187
|
+
const lines = [];
|
|
188
|
+
lines.push("# Agent Task: UI E2E Recovery");
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("## Goal");
|
|
191
|
+
lines.push("Fix target project implementation so ui acceptance passes.");
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push("## Constraints");
|
|
194
|
+
lines.push("- Must run ui acceptance after code changes.");
|
|
195
|
+
lines.push("- Do not bypass by lowering thresholds unless explicitly requested.");
|
|
196
|
+
lines.push("- Prioritize real component/contract/recipe fixes.");
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push("## Context");
|
|
199
|
+
lines.push(`- targetProject: ${normalizeSlash(payload.targetProject || "")}`);
|
|
200
|
+
lines.push(`- mode: ${payload.mode || "single"}`);
|
|
201
|
+
lines.push(`- profile: ${payload.profile || "standard"}`);
|
|
202
|
+
lines.push(`- autoEnsureOnMiss: ${payload.autoEnsureOnMiss ? "true" : "false"}`);
|
|
203
|
+
lines.push(`- fixLoop: ${Number(payload.fixLoop || 0)}`);
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push("## Cases");
|
|
206
|
+
(payload.cases || []).forEach((entry, idx) => {
|
|
207
|
+
lines.push(`### Case ${idx + 1}`);
|
|
208
|
+
lines.push(`- cacheKey: ${entry.cacheKey || ""}`);
|
|
209
|
+
lines.push(`- targetPath: ${normalizeSlash(entry.targetPath || "")}`);
|
|
210
|
+
lines.push(`- reason: ${entry.reason || "unknown"}`);
|
|
211
|
+
if (entry.attemptLogs && entry.attemptLogs.length) {
|
|
212
|
+
lines.push("- attemptLogs:");
|
|
213
|
+
entry.attemptLogs.forEach((log) => {
|
|
214
|
+
lines.push(
|
|
215
|
+
` - attempt ${log.attempt}: ${log.ok ? "ok" : "fail"}${log.reason ? ` (${log.reason})` : ""}`
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
lines.push("");
|
|
220
|
+
});
|
|
221
|
+
lines.push("## Required Command");
|
|
222
|
+
lines.push("Run this command in toolchain repo after fixes:");
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("```bash");
|
|
225
|
+
lines.push(payload.retryCommand || "npm run figma:ui:e2e:cross -- --target-project=<...>");
|
|
226
|
+
lines.push("```");
|
|
227
|
+
lines.push("");
|
|
228
|
+
lines.push("## Completion Criteria");
|
|
229
|
+
lines.push("- e2e command exits with code 0");
|
|
230
|
+
lines.push("- summaryStatus is healthy");
|
|
231
|
+
lines.push("- no unresolved blocking items");
|
|
232
|
+
lines.push("");
|
|
233
|
+
|
|
234
|
+
fs.mkdirSync(path.dirname(taskPath), { recursive: true });
|
|
235
|
+
fs.writeFileSync(taskPath, `${lines.join("\n")}\n`, "utf8");
|
|
236
|
+
return taskPath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseCacheKey(cacheKey) {
|
|
240
|
+
const value = String(cacheKey || "").trim();
|
|
241
|
+
const [fileKey, nodeId] = value.split("#");
|
|
242
|
+
if (!fileKey || !nodeId) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
fileKey,
|
|
247
|
+
nodeId,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildFigmaUrl(fileKey, nodeId) {
|
|
252
|
+
const normalizedNodeId = String(nodeId || "").replace(/:/g, "-");
|
|
253
|
+
return `https://www.figma.com/file/${fileKey}/auto-e2e?node-id=${normalizedNodeId}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function readTargetIndexItem(targetProject, cacheKey) {
|
|
257
|
+
const indexPath = path.join(targetProject, "figma-cache", "index.json");
|
|
258
|
+
const index = readJsonOrNull(indexPath);
|
|
259
|
+
const items = index && index.items && typeof index.items === "object" ? index.items : {};
|
|
260
|
+
return items[cacheKey] || null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function ensureCacheViaFigmaMcp(targetProject, cacheKey, options) {
|
|
264
|
+
const parsed = parseCacheKey(cacheKey);
|
|
265
|
+
if (!parsed) {
|
|
266
|
+
throw new Error(`invalid cacheKey for auto ensure: ${cacheKey}`);
|
|
267
|
+
}
|
|
268
|
+
const cliPath = path.join(
|
|
269
|
+
targetProject,
|
|
270
|
+
"node_modules",
|
|
271
|
+
"figma-cache-toolchain",
|
|
272
|
+
"bin",
|
|
273
|
+
"figma-cache.js"
|
|
274
|
+
);
|
|
275
|
+
if (!fs.existsSync(cliPath)) {
|
|
276
|
+
throw new Error("figma-cache cli not found in target project node_modules");
|
|
277
|
+
}
|
|
278
|
+
const figmaUrl = buildFigmaUrl(parsed.fileKey, parsed.nodeId);
|
|
279
|
+
const args = [
|
|
280
|
+
`node "${cliPath}"`,
|
|
281
|
+
"ensure",
|
|
282
|
+
`"${figmaUrl}"`,
|
|
283
|
+
"--source=figma-mcp",
|
|
284
|
+
`--completeness=${options.completeness}`,
|
|
285
|
+
];
|
|
286
|
+
if (options.allowSkeletonWithFigmaMcp) {
|
|
287
|
+
args.push("--allow-skeleton-with-figma-mcp");
|
|
288
|
+
}
|
|
289
|
+
runCommand(args.join(" "), targetProject);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function bootstrapContractIfNeeded(targetProject, options) {
|
|
293
|
+
if (!options.autoBootstrapContract) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const contractPath = path.join(targetProject, "figma-cache", "adapters", "ui-adapter.contract.json");
|
|
297
|
+
if (fs.existsSync(contractPath)) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const templatePath = path.join(
|
|
301
|
+
targetProject,
|
|
302
|
+
"node_modules",
|
|
303
|
+
"figma-cache-toolchain",
|
|
304
|
+
"cursor-bootstrap",
|
|
305
|
+
"examples",
|
|
306
|
+
"ui-adapter.contract.template.json"
|
|
307
|
+
);
|
|
308
|
+
if (fs.existsSync(templatePath)) {
|
|
309
|
+
fs.mkdirSync(path.dirname(contractPath), { recursive: true });
|
|
310
|
+
fs.copyFileSync(templatePath, contractPath);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runSingleCase(input, context) {
|
|
315
|
+
const { targetProject, acceptScriptPath, options } = context;
|
|
316
|
+
const item = {
|
|
317
|
+
cacheKey: input.cacheKey || "",
|
|
318
|
+
fileKey: input.fileKey || "",
|
|
319
|
+
nodeId: input.nodeId || "",
|
|
320
|
+
target: input.target || options.target || "",
|
|
321
|
+
minScore: Number.isFinite(Number(input.minScore)) ? Number(input.minScore) : options.minScore,
|
|
322
|
+
maxWarnings: Number.isFinite(Number(input.maxWarnings))
|
|
323
|
+
? Number(input.maxWarnings)
|
|
324
|
+
: options.maxWarnings,
|
|
325
|
+
maxDiffs: Number.isFinite(Number(input.maxDiffs)) ? Number(input.maxDiffs) : options.maxDiffs,
|
|
326
|
+
};
|
|
327
|
+
const cacheKey = item.cacheKey || resolveCacheKey(item);
|
|
328
|
+
if (!cacheKey) {
|
|
329
|
+
throw new Error("batch item missing cacheKey or (fileKey + nodeId)");
|
|
330
|
+
}
|
|
331
|
+
const targetPath = item.target ? resolveMaybeAbsolutePath(item.target) : "";
|
|
332
|
+
if (!targetPath) {
|
|
333
|
+
throw new Error(`batch item ${cacheKey} missing target path`);
|
|
334
|
+
}
|
|
335
|
+
if (!fs.existsSync(targetPath)) {
|
|
336
|
+
throw new Error(`batch item ${cacheKey} target path does not exist: ${targetPath}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const acceptArgs = [
|
|
340
|
+
`--cacheKey=${cacheKey}`,
|
|
341
|
+
`--target=${targetPath}`,
|
|
342
|
+
`--min-score=${item.minScore}`,
|
|
343
|
+
`--max-warnings=${item.maxWarnings}`,
|
|
344
|
+
`--max-diffs=${item.maxDiffs}`,
|
|
345
|
+
];
|
|
346
|
+
const env = {};
|
|
347
|
+
if (options.profile) {
|
|
348
|
+
env.FIGMA_UI_PROFILE = options.profile;
|
|
349
|
+
}
|
|
350
|
+
let attempt = 0;
|
|
351
|
+
let lastError = "";
|
|
352
|
+
let acceptanceJson = null;
|
|
353
|
+
const attemptLogs = [];
|
|
354
|
+
const maxAttempts = 1 + options.fixLoop;
|
|
355
|
+
while (attempt < maxAttempts) {
|
|
356
|
+
attempt += 1;
|
|
357
|
+
let itemExists = !!readTargetIndexItem(targetProject, cacheKey);
|
|
358
|
+
if (!itemExists && options.autoEnsureOnMiss) {
|
|
359
|
+
ensureCacheViaFigmaMcp(targetProject, cacheKey, options);
|
|
360
|
+
itemExists = !!readTargetIndexItem(targetProject, cacheKey);
|
|
361
|
+
}
|
|
362
|
+
if (!itemExists) {
|
|
363
|
+
lastError = `cacheKey miss: ${cacheKey}. try --auto-ensure-on-miss or pre-populate cache`;
|
|
364
|
+
attemptLogs.push({ attempt, ok: false, reason: lastError });
|
|
365
|
+
if (attempt >= maxAttempts) {
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const acceptanceOutput = runCommand(
|
|
372
|
+
`node "${acceptScriptPath}" ${acceptArgs.join(" ")}`,
|
|
373
|
+
targetProject,
|
|
374
|
+
env
|
|
375
|
+
);
|
|
376
|
+
try {
|
|
377
|
+
acceptanceJson = JSON.parse(acceptanceOutput);
|
|
378
|
+
} catch {}
|
|
379
|
+
if (
|
|
380
|
+
!options.allowSkippedCodeLevelComparison &&
|
|
381
|
+
acceptanceJson &&
|
|
382
|
+
Array.isArray(acceptanceJson.warnings) &&
|
|
383
|
+
acceptanceJson.warnings.some((entry) =>
|
|
384
|
+
/code-level comparison skipped/i.test(String(entry || ""))
|
|
385
|
+
)
|
|
386
|
+
) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`acceptance produced skipped code-level comparison for ${cacheKey}; target linkage is invalid`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
attemptLogs.push({ attempt, ok: true });
|
|
392
|
+
return {
|
|
393
|
+
ok: true,
|
|
394
|
+
cacheKey,
|
|
395
|
+
targetPath: normalizeSlash(targetPath),
|
|
396
|
+
acceptance: acceptanceJson,
|
|
397
|
+
attempts: attempt,
|
|
398
|
+
attemptLogs,
|
|
399
|
+
};
|
|
400
|
+
} catch (error) {
|
|
401
|
+
lastError = error && error.message ? error.message : "unknown acceptance error";
|
|
402
|
+
attemptLogs.push({ attempt, ok: false, reason: lastError });
|
|
403
|
+
if (attempt >= maxAttempts) {
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
// self-healing retry: re-bootstrap contract + refresh cache evidence when enabled
|
|
407
|
+
bootstrapContractIfNeeded(targetProject, options);
|
|
408
|
+
if (options.autoEnsureOnMiss) {
|
|
409
|
+
try {
|
|
410
|
+
ensureCacheViaFigmaMcp(targetProject, cacheKey, options);
|
|
411
|
+
} catch {}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
throw new Error(
|
|
416
|
+
`acceptance failed after ${maxAttempts} attempts for ${cacheKey}: ${lastError}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function run() {
|
|
421
|
+
const options = parseArgs(process.argv.slice(2));
|
|
422
|
+
const targetProject = resolveMaybeAbsolutePath(options.targetProject);
|
|
423
|
+
if (!targetProject || !fs.existsSync(targetProject)) {
|
|
424
|
+
console.error("cross-project-e2e failed: --target-project is required and must exist");
|
|
425
|
+
process.exit(FAIL_EXIT_CODE);
|
|
426
|
+
}
|
|
427
|
+
const isBatchMode = !!options.batchFile;
|
|
428
|
+
if (!isBatchMode) {
|
|
429
|
+
const targetPath = options.target ? resolveMaybeAbsolutePath(options.target) : "";
|
|
430
|
+
if (!targetPath) {
|
|
431
|
+
console.error("cross-project-e2e failed: --target is required for real component validation");
|
|
432
|
+
process.exit(FAIL_EXIT_CODE);
|
|
433
|
+
}
|
|
434
|
+
if (!fs.existsSync(targetPath)) {
|
|
435
|
+
console.error(`cross-project-e2e failed: --target path does not exist: ${targetPath}`);
|
|
436
|
+
process.exit(FAIL_EXIT_CODE);
|
|
437
|
+
}
|
|
438
|
+
const cacheKey = resolveCacheKey(options);
|
|
439
|
+
if (!cacheKey) {
|
|
440
|
+
console.error("cross-project-e2e failed: provide --cacheKey or (--fileKey + --nodeId)");
|
|
441
|
+
process.exit(FAIL_EXIT_CODE);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let tarballPath = "";
|
|
446
|
+
let taskPayload = null;
|
|
447
|
+
try {
|
|
448
|
+
tarballPath = npmPackAndGetTarball();
|
|
449
|
+
runCommand(`npm i -D "${tarballPath}"`, targetProject);
|
|
450
|
+
|
|
451
|
+
const acceptScript = path.join(
|
|
452
|
+
targetProject,
|
|
453
|
+
"node_modules",
|
|
454
|
+
"figma-cache-toolchain",
|
|
455
|
+
"scripts",
|
|
456
|
+
"ui-auto-acceptance.js"
|
|
457
|
+
);
|
|
458
|
+
if (!fs.existsSync(acceptScript)) {
|
|
459
|
+
throw new Error("ui-auto-acceptance.js not found in installed package; check package files field");
|
|
460
|
+
}
|
|
461
|
+
bootstrapContractIfNeeded(targetProject, options);
|
|
462
|
+
|
|
463
|
+
const cases = isBatchMode
|
|
464
|
+
? (() => {
|
|
465
|
+
const batchPath = resolveMaybeAbsolutePath(options.batchFile);
|
|
466
|
+
const payload = readJsonOrNull(batchPath);
|
|
467
|
+
if (!Array.isArray(payload) || !payload.length) {
|
|
468
|
+
throw new Error(`invalid batch file: ${batchPath}`);
|
|
469
|
+
}
|
|
470
|
+
return payload;
|
|
471
|
+
})()
|
|
472
|
+
: [
|
|
473
|
+
{
|
|
474
|
+
cacheKey: options.cacheKey,
|
|
475
|
+
fileKey: options.fileKey,
|
|
476
|
+
nodeId: options.nodeId,
|
|
477
|
+
target: options.target,
|
|
478
|
+
minScore: options.minScore,
|
|
479
|
+
maxWarnings: options.maxWarnings,
|
|
480
|
+
maxDiffs: options.maxDiffs,
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
const caseResults = [];
|
|
485
|
+
const caseFailures = [];
|
|
486
|
+
cases.forEach((entry, indexNo) => {
|
|
487
|
+
try {
|
|
488
|
+
caseResults.push(
|
|
489
|
+
runSingleCase(entry, {
|
|
490
|
+
targetProject,
|
|
491
|
+
acceptScriptPath: acceptScript,
|
|
492
|
+
options,
|
|
493
|
+
})
|
|
494
|
+
);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
caseFailures.push({
|
|
497
|
+
index: indexNo,
|
|
498
|
+
cacheKey: entry && (entry.cacheKey || resolveCacheKey(entry)),
|
|
499
|
+
targetPath: entry && entry.target,
|
|
500
|
+
attemptLogs: [],
|
|
501
|
+
reason: error.message,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
if (caseFailures.length) {
|
|
506
|
+
taskPayload = {
|
|
507
|
+
targetProject,
|
|
508
|
+
mode: isBatchMode ? "batch" : "single",
|
|
509
|
+
profile: options.profile || "standard",
|
|
510
|
+
autoEnsureOnMiss: options.autoEnsureOnMiss,
|
|
511
|
+
fixLoop: options.fixLoop,
|
|
512
|
+
cases: caseFailures,
|
|
513
|
+
retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
|
|
514
|
+
targetProject
|
|
515
|
+
)}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
|
|
516
|
+
options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
|
|
517
|
+
}${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""}`,
|
|
518
|
+
};
|
|
519
|
+
throw new Error(`batch cases failed: ${JSON.stringify(caseFailures)}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const reportBase = path.join(targetProject, "figma-cache", "reports");
|
|
523
|
+
const output = {
|
|
524
|
+
ok: true,
|
|
525
|
+
generatedAt: new Date().toISOString(),
|
|
526
|
+
targetProject,
|
|
527
|
+
mode: isBatchMode ? "batch" : "single",
|
|
528
|
+
profile: options.profile || null,
|
|
529
|
+
autoEnsureOnMiss: options.autoEnsureOnMiss,
|
|
530
|
+
allowSkeletonWithFigmaMcp: options.allowSkeletonWithFigmaMcp,
|
|
531
|
+
completeness: options.completeness,
|
|
532
|
+
tarballPath,
|
|
533
|
+
reports: {
|
|
534
|
+
preflight: path.join(reportBase, "runtime", "ui-preflight-report.json"),
|
|
535
|
+
audit: path.join(reportBase, "runtime", "ui-1to1-report.json"),
|
|
536
|
+
summary: path.join(reportBase, "runtime", "ui-quality-summary.json"),
|
|
537
|
+
},
|
|
538
|
+
cases: caseResults,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
const summary = readJsonOrNull(output.reports.summary);
|
|
542
|
+
if (summary) {
|
|
543
|
+
output.summaryMetrics = summary.metrics || null;
|
|
544
|
+
output.summaryStatus = summary.trend && summary.trend.status;
|
|
545
|
+
}
|
|
546
|
+
console.log(JSON.stringify(output, null, 2));
|
|
547
|
+
} catch (error) {
|
|
548
|
+
let taskPath = "";
|
|
549
|
+
if (options.emitAgentTaskOnFail) {
|
|
550
|
+
try {
|
|
551
|
+
const payload =
|
|
552
|
+
taskPayload ||
|
|
553
|
+
({
|
|
554
|
+
targetProject,
|
|
555
|
+
mode: isBatchMode ? "batch" : "single",
|
|
556
|
+
profile: options.profile || "standard",
|
|
557
|
+
autoEnsureOnMiss: options.autoEnsureOnMiss,
|
|
558
|
+
fixLoop: options.fixLoop,
|
|
559
|
+
cases: [
|
|
560
|
+
{
|
|
561
|
+
cacheKey: resolveCacheKey(options),
|
|
562
|
+
targetPath: options.target,
|
|
563
|
+
reason: error.message,
|
|
564
|
+
},
|
|
565
|
+
],
|
|
566
|
+
retryCommand: `npm run figma:ui:e2e:cross -- --target-project=${normalizeSlash(
|
|
567
|
+
targetProject
|
|
568
|
+
)}${options.batchFile ? ` --batch-file=${normalizeSlash(options.batchFile)}` : ""}${
|
|
569
|
+
options.cacheKey ? ` --cacheKey=${options.cacheKey}` : ""
|
|
570
|
+
}${options.fileKey ? ` --fileKey=${options.fileKey}` : ""}${
|
|
571
|
+
options.nodeId ? ` --nodeId=${options.nodeId}` : ""
|
|
572
|
+
}${options.target ? ` --target=${normalizeSlash(options.target)}` : ""}${
|
|
573
|
+
options.autoEnsureOnMiss ? " --auto-ensure-on-miss" : ""
|
|
574
|
+
}${options.fixLoop ? ` --fix-loop=${options.fixLoop}` : ""} --emit-agent-task-on-fail`,
|
|
575
|
+
});
|
|
576
|
+
taskPath = writeAgentTask(targetProject, options, payload);
|
|
577
|
+
} catch {}
|
|
578
|
+
}
|
|
579
|
+
console.error("cross-project-e2e failed:");
|
|
580
|
+
console.error(`- ${error.message}`);
|
|
581
|
+
if (taskPath) {
|
|
582
|
+
console.error(`- agent task emitted: ${normalizeSlash(taskPath)}`);
|
|
583
|
+
}
|
|
584
|
+
process.exit(FAIL_EXIT_CODE);
|
|
585
|
+
} finally {
|
|
586
|
+
if (tarballPath && !options.keepPackage) {
|
|
587
|
+
try {
|
|
588
|
+
fs.unlinkSync(tarballPath);
|
|
589
|
+
} catch {}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
run();
|