figma-cache-toolchain 1.4.3 → 1.4.4
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/LICENSE +21 -21
- package/README.md +72 -84
- package/README.owner.md +37 -0
- package/cursor-bootstrap/AGENT-SETUP-PROMPT.md +53 -62
- package/cursor-bootstrap/examples/README.md +5 -3
- package/cursor-bootstrap/figma-cache.config.example.js +238 -138
- package/cursor-bootstrap/rules/01-figma-cache-core.mdc +42 -5
- package/cursor-bootstrap/skills/figma-mcp-local-cache/SKILL.md +10 -5
- package/figma-cache/{README.md → docs/README.md} +42 -19
- package/figma-cache/{colleague-guide-zh.md → docs/colleague-guide-zh.md} +8 -7
- package/figma-cache/docs/figma-cache-adapter-hint.md +14 -0
- package/figma-cache/figma-cache.js +250 -881
- package/figma-cache/js/backfill-cli.js +50 -0
- package/figma-cache/js/budget-cli.js +108 -0
- package/figma-cache/js/cursor-bootstrap-cli.js +178 -0
- package/figma-cache/js/entry-files.js +149 -0
- package/figma-cache/js/flow-cli.js +227 -0
- package/figma-cache/js/index-store.js +86 -0
- package/figma-cache/js/project-config.js +97 -0
- package/figma-cache/js/upsert-core.js +156 -0
- package/figma-cache/js/validate-cli.js +233 -0
- package/package.json +72 -55
- /package/figma-cache/{flow-edge-taxonomy.md → docs/flow-edge-taxonomy.md} +0 -0
- /package/figma-cache/{link-normalization-spec.md → docs/link-normalization-spec.md} +0 -0
|
@@ -4,10 +4,49 @@ const fs = require("fs");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const { createRequire } = require("module");
|
|
6
6
|
const { URL } = require("url");
|
|
7
|
+
const { handleFlowCommand } = require("./js/flow-cli");
|
|
8
|
+
const { validateMcpRawEvidence, validateIndex } = require("./js/validate-cli");
|
|
9
|
+
const { buildBudgetReport } = require("./js/budget-cli");
|
|
10
|
+
const { createIndexStore } = require("./js/index-store");
|
|
11
|
+
const { copyCursorBootstrap } = require("./js/cursor-bootstrap-cli");
|
|
12
|
+
const { createEntryFilesService } = require("./js/entry-files");
|
|
13
|
+
const { backfillFromIterations } = require("./js/backfill-cli");
|
|
14
|
+
const { createUpsertService } = require("./js/upsert-core");
|
|
15
|
+
const { createProjectConfigService } = require("./js/project-config");
|
|
7
16
|
|
|
8
17
|
const ROOT = process.cwd();
|
|
9
18
|
const NORMALIZATION_VERSION = 1;
|
|
10
19
|
const SCHEMA_VERSION = 2;
|
|
20
|
+
const DEFAULT_COMPLETENESS = Object.freeze([
|
|
21
|
+
"layout",
|
|
22
|
+
"text",
|
|
23
|
+
"tokens",
|
|
24
|
+
"interactions",
|
|
25
|
+
"states",
|
|
26
|
+
"accessibility",
|
|
27
|
+
]);
|
|
28
|
+
const COMPLETENESS_ALL_DIMENSIONS = Object.freeze([
|
|
29
|
+
"layout",
|
|
30
|
+
"text",
|
|
31
|
+
"tokens",
|
|
32
|
+
"interactions",
|
|
33
|
+
"states",
|
|
34
|
+
"accessibility",
|
|
35
|
+
"flow",
|
|
36
|
+
"assets",
|
|
37
|
+
]);
|
|
38
|
+
const COMPLETENESS_TOOL_REQUIREMENTS = Object.freeze({
|
|
39
|
+
layout: Object.freeze([
|
|
40
|
+
Object.freeze(["get_metadata", "get_design_context"]),
|
|
41
|
+
]),
|
|
42
|
+
text: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
43
|
+
tokens: Object.freeze([Object.freeze(["get_variable_defs"])]),
|
|
44
|
+
interactions: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
45
|
+
states: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
46
|
+
accessibility: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
47
|
+
flow: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
48
|
+
assets: Object.freeze([Object.freeze(["get_design_context"])]),
|
|
49
|
+
});
|
|
11
50
|
|
|
12
51
|
function parsePositiveInt(input, fallback) {
|
|
13
52
|
const n = Number(input);
|
|
@@ -40,15 +79,15 @@ const INDEX_FILE_NAME = process.env.FIGMA_CACHE_INDEX_FILE || "index.json";
|
|
|
40
79
|
const DEFAULT_FLOW_ID = process.env.FIGMA_DEFAULT_FLOW || "";
|
|
41
80
|
const DEFAULT_STALE_DAYS = parsePositiveInt(
|
|
42
81
|
process.env.FIGMA_CACHE_STALE_DAYS,
|
|
43
|
-
14
|
|
82
|
+
14,
|
|
44
83
|
);
|
|
45
84
|
|
|
46
85
|
const CACHE_DIR = resolveMaybeAbsolutePath(CACHE_DIR_INPUT);
|
|
47
|
-
/**
|
|
86
|
+
/** 涓?`figma-cache/figma-cache.js` 鍚岀骇鐨?`cursor-bootstrap/`锛堥殢 npm 鍖呭垎鍙戯級 */
|
|
48
87
|
const CURSOR_BOOTSTRAP_DIR = path.join(__dirname, "..", "cursor-bootstrap");
|
|
49
88
|
const ITERATIONS_DIR = resolveMaybeAbsolutePath(ITERATIONS_DIR_INPUT);
|
|
50
89
|
|
|
51
|
-
/**
|
|
90
|
+
/** 褰撳墠瀹夎鍖呭湪 package.json 涓殑 name锛堢敤浜庡啓鍏?AGENT-SETUP-PROMPT.md锛?*/
|
|
52
91
|
function readSelfNpmPackageName() {
|
|
53
92
|
try {
|
|
54
93
|
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
@@ -64,89 +103,45 @@ const INDEX_PATH = path.isAbsolute(INDEX_FILE_NAME)
|
|
|
64
103
|
: path.join(CACHE_DIR, INDEX_FILE_NAME);
|
|
65
104
|
const CACHE_BASE_FOR_STORAGE = toProjectRelativeOrAbsolute(CACHE_DIR);
|
|
66
105
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
memoProjectConfig = {};
|
|
108
|
-
memoProjectConfigPath = null;
|
|
109
|
-
return memoProjectConfig;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* After generic entry files exist, run optional hooks.postEnsure from project config.
|
|
114
|
-
* Never throws; never changes process exit code of the caller.
|
|
115
|
-
* @param {string} cacheKey
|
|
116
|
-
* @param {object} item index item
|
|
117
|
-
*/
|
|
118
|
-
function runPostEnsureHook(cacheKey, item) {
|
|
119
|
-
if (!item || !item.paths) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const cfg = loadProjectConfig();
|
|
123
|
-
const hooks = cfg && cfg.hooks;
|
|
124
|
-
if (!hooks || typeof hooks.postEnsure !== "function") {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
const ctx = {
|
|
128
|
-
cacheKey,
|
|
129
|
-
fileKey: item.fileKey,
|
|
130
|
-
nodeId: item.nodeId == null ? null : item.nodeId,
|
|
131
|
-
scope: item.scope,
|
|
132
|
-
url: item.url == null ? "" : String(item.url),
|
|
133
|
-
source: item.source == null ? "" : String(item.source),
|
|
134
|
-
syncedAt: item.syncedAt == null ? "" : String(item.syncedAt),
|
|
135
|
-
completeness: Array.isArray(item.completeness) ? item.completeness : [],
|
|
136
|
-
paths: {
|
|
137
|
-
raw: item.paths.raw,
|
|
138
|
-
spec: item.paths.spec,
|
|
139
|
-
meta: item.paths.meta,
|
|
140
|
-
stateMap: item.paths.stateMap,
|
|
141
|
-
},
|
|
142
|
-
root: ROOT,
|
|
143
|
-
};
|
|
144
|
-
try {
|
|
145
|
-
hooks.postEnsure(ctx);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
console.error(`[figma-cache] hooks.postEnsure: ${err.message}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
106
|
+
const indexStore = createIndexStore({
|
|
107
|
+
fs,
|
|
108
|
+
CACHE_DIR,
|
|
109
|
+
INDEX_PATH,
|
|
110
|
+
SCHEMA_VERSION,
|
|
111
|
+
NORMALIZATION_VERSION,
|
|
112
|
+
});
|
|
113
|
+
const {
|
|
114
|
+
ensureCacheDir,
|
|
115
|
+
buildEmptyIndex,
|
|
116
|
+
normalizeIndexShape,
|
|
117
|
+
readIndex,
|
|
118
|
+
writeIndex,
|
|
119
|
+
getItem,
|
|
120
|
+
} = indexStore;
|
|
121
|
+
|
|
122
|
+
const projectConfigService = createProjectConfigService({
|
|
123
|
+
fs,
|
|
124
|
+
path,
|
|
125
|
+
ROOT,
|
|
126
|
+
createRequire,
|
|
127
|
+
resolveMaybeAbsolutePath,
|
|
128
|
+
normalizeSlash,
|
|
129
|
+
});
|
|
130
|
+
const { loadProjectConfig, runPostEnsureHook, getProjectConfigPath } =
|
|
131
|
+
projectConfigService;
|
|
132
|
+
|
|
133
|
+
const upsertService = createUpsertService({
|
|
134
|
+
URL,
|
|
135
|
+
NORMALIZATION_VERSION,
|
|
136
|
+
CACHE_BASE_FOR_STORAGE,
|
|
137
|
+
DEFAULT_COMPLETENESS,
|
|
138
|
+
normalizeCompletenessList,
|
|
139
|
+
normalizeIndexShape,
|
|
140
|
+
readIndex,
|
|
141
|
+
getItem,
|
|
142
|
+
writeIndex,
|
|
143
|
+
});
|
|
144
|
+
const { normalizeFigmaUrl, previewUpsertByUrl, upsertByUrl } = upsertService;
|
|
150
145
|
|
|
151
146
|
function resolveFlowIdFromArgs(rest) {
|
|
152
147
|
const flowArg = rest.find((x) => x.startsWith("--flow="));
|
|
@@ -159,479 +154,47 @@ function resolveFlowIdFromArgs(rest) {
|
|
|
159
154
|
return "";
|
|
160
155
|
}
|
|
161
156
|
|
|
162
|
-
function
|
|
163
|
-
if (!
|
|
164
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function readIndex() {
|
|
169
|
-
ensureCacheDir();
|
|
170
|
-
if (!fs.existsSync(INDEX_PATH)) {
|
|
171
|
-
return buildEmptyIndex();
|
|
172
|
-
}
|
|
173
|
-
const raw = fs.readFileSync(INDEX_PATH, "utf8");
|
|
174
|
-
return normalizeIndexShape(JSON.parse(raw));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function buildEmptyIndex() {
|
|
178
|
-
return {
|
|
179
|
-
schemaVersion: SCHEMA_VERSION,
|
|
180
|
-
version: 1,
|
|
181
|
-
normalizationVersion: NORMALIZATION_VERSION,
|
|
182
|
-
updatedAt: null,
|
|
183
|
-
flows: {},
|
|
184
|
-
items: {},
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function normalizeIndexShape(index) {
|
|
189
|
-
if (!index || typeof index !== "object") {
|
|
190
|
-
return buildEmptyIndex();
|
|
191
|
-
}
|
|
192
|
-
if (!index.schemaVersion) {
|
|
193
|
-
index.schemaVersion = SCHEMA_VERSION;
|
|
194
|
-
}
|
|
195
|
-
if (!index.flows || typeof index.flows !== "object") {
|
|
196
|
-
index.flows = {};
|
|
197
|
-
}
|
|
198
|
-
if (!index.items || typeof index.items !== "object") {
|
|
199
|
-
index.items = {};
|
|
200
|
-
}
|
|
201
|
-
if (!index.version) {
|
|
202
|
-
index.version = 1;
|
|
203
|
-
}
|
|
204
|
-
if (!index.normalizationVersion) {
|
|
205
|
-
index.normalizationVersion = NORMALIZATION_VERSION;
|
|
206
|
-
}
|
|
207
|
-
return index;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function writeIndex(index) {
|
|
211
|
-
const normalized = normalizeIndexShape(index);
|
|
212
|
-
normalized.version = 1;
|
|
213
|
-
normalized.schemaVersion = SCHEMA_VERSION;
|
|
214
|
-
normalized.normalizationVersion = NORMALIZATION_VERSION;
|
|
215
|
-
normalized.updatedAt = new Date().toISOString();
|
|
216
|
-
fs.writeFileSync(INDEX_PATH, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function sanitizeNodeId(nodeId) {
|
|
220
|
-
return String(nodeId).replace(/:/g, "-");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function normalizeNodeIdValue(nodeId) {
|
|
224
|
-
const raw = String(nodeId).trim();
|
|
225
|
-
const dashPattern = /^(\d+)-(\d+)$/;
|
|
226
|
-
if (dashPattern.test(raw)) {
|
|
227
|
-
return raw.replace(dashPattern, "$1:$2");
|
|
228
|
-
}
|
|
229
|
-
return raw;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function normalizeFigmaUrl(inputUrl) {
|
|
233
|
-
let parsed;
|
|
234
|
-
try {
|
|
235
|
-
parsed = new URL(inputUrl);
|
|
236
|
-
} catch (error) {
|
|
237
|
-
throw new Error(`非法 URL: ${inputUrl}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const hostOk = /(^|\.)figma\.com$/i.test(parsed.hostname);
|
|
241
|
-
if (!hostOk) {
|
|
242
|
-
throw new Error(`非 Figma 域名: ${parsed.hostname}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
246
|
-
const routeType = parts[0];
|
|
247
|
-
const fileKey = parts[1];
|
|
248
|
-
if (!["file", "design"].includes(routeType) || !fileKey) {
|
|
249
|
-
throw new Error(`无法从路径提取 fileKey: ${parsed.pathname}`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const nodeIdRaw = parsed.searchParams.get("node-id");
|
|
253
|
-
const nodeId = nodeIdRaw ? normalizeNodeIdValue(decodeURIComponent(nodeIdRaw)) : null;
|
|
254
|
-
const isNodeScope = !!nodeId;
|
|
255
|
-
const scope = isNodeScope ? "node" : "file";
|
|
256
|
-
const cacheKey = isNodeScope ? `${fileKey}#${nodeId}` : `${fileKey}#__FILE__`;
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
fileKey,
|
|
260
|
-
nodeId,
|
|
261
|
-
scope,
|
|
262
|
-
cacheKey,
|
|
263
|
-
normalizedUrl: isNodeScope
|
|
264
|
-
? `https://www.figma.com/file/${fileKey}/?node-id=${encodeURIComponent(nodeId)}`
|
|
265
|
-
: `https://www.figma.com/file/${fileKey}/`,
|
|
266
|
-
originalUrl: inputUrl,
|
|
267
|
-
normalizationVersion: NORMALIZATION_VERSION,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function buildPaths(normalized) {
|
|
272
|
-
if (normalized.scope === "file") {
|
|
273
|
-
return {
|
|
274
|
-
meta: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/meta.json`,
|
|
275
|
-
spec: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/spec.md`,
|
|
276
|
-
stateMap: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/state-map.md`,
|
|
277
|
-
raw: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/raw.json`,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const safeNode = sanitizeNodeId(normalized.nodeId);
|
|
282
|
-
return {
|
|
283
|
-
meta: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/meta.json`,
|
|
284
|
-
spec: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/spec.md`,
|
|
285
|
-
stateMap: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/state-map.md`,
|
|
286
|
-
raw: `${CACHE_BASE_FOR_STORAGE}/files/${normalized.fileKey}/nodes/${safeNode}/raw.json`,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function getItem(index, cacheKey) {
|
|
291
|
-
const normalized = normalizeIndexShape(index);
|
|
292
|
-
return normalized.items && normalized.items[cacheKey] ? normalized.items[cacheKey] : null;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function upsertByUrl(inputUrl, extra) {
|
|
296
|
-
const normalized = normalizeFigmaUrl(inputUrl);
|
|
297
|
-
const index = normalizeIndexShape(readIndex());
|
|
298
|
-
const oldItem = getItem(index, normalized.cacheKey);
|
|
299
|
-
const mergedUrls = Array.from(
|
|
300
|
-
new Set([...(oldItem ? oldItem.originalUrls || [] : []), normalized.originalUrl])
|
|
301
|
-
);
|
|
302
|
-
const now = new Date().toISOString();
|
|
303
|
-
|
|
304
|
-
const item = {
|
|
305
|
-
fileKey: normalized.fileKey,
|
|
306
|
-
nodeId: normalized.nodeId,
|
|
307
|
-
scope: normalized.scope,
|
|
308
|
-
url: normalized.normalizedUrl,
|
|
309
|
-
originalUrls: mergedUrls,
|
|
310
|
-
normalizationVersion: NORMALIZATION_VERSION,
|
|
311
|
-
paths: oldItem && oldItem.paths ? oldItem.paths : buildPaths(normalized),
|
|
312
|
-
syncedAt: now,
|
|
313
|
-
completeness: Array.isArray(extra.completeness)
|
|
314
|
-
? extra.completeness
|
|
315
|
-
: oldItem && Array.isArray(oldItem.completeness)
|
|
316
|
-
? oldItem.completeness
|
|
317
|
-
: [],
|
|
318
|
-
source: extra.source || (oldItem && oldItem.source) || "manual",
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
index.items = index.items || {};
|
|
322
|
-
index.items[normalized.cacheKey] = item;
|
|
323
|
-
writeIndex(index);
|
|
324
|
-
return { normalized, item };
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function ensureFileWithDefault(relativePath, fallbackContent) {
|
|
328
|
-
const absPath = resolveMaybeAbsolutePath(relativePath);
|
|
329
|
-
const dir = path.dirname(absPath);
|
|
330
|
-
if (!fs.existsSync(dir)) {
|
|
331
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
332
|
-
}
|
|
333
|
-
if (!fs.existsSync(absPath)) {
|
|
334
|
-
fs.writeFileSync(absPath, fallbackContent, "utf8");
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function ensureEntryFiles(item) {
|
|
339
|
-
ensureFileWithDefault(
|
|
340
|
-
item.paths.meta,
|
|
341
|
-
`${JSON.stringify(
|
|
342
|
-
{
|
|
343
|
-
fileKey: item.fileKey,
|
|
344
|
-
nodeId: item.nodeId,
|
|
345
|
-
scope: item.scope,
|
|
346
|
-
source: item.source,
|
|
347
|
-
syncedAt: item.syncedAt,
|
|
348
|
-
},
|
|
349
|
-
null,
|
|
350
|
-
2
|
|
351
|
-
)}\n`
|
|
352
|
-
);
|
|
353
|
-
ensureFileWithDefault(
|
|
354
|
-
item.paths.spec,
|
|
355
|
-
`# Figma Spec\n\n- fileKey: ${item.fileKey}\n- scope: ${item.scope}\n- nodeId: ${item.nodeId || "N/A"}\n- source: ${item.source}\n- syncedAt: ${item.syncedAt}\n`
|
|
356
|
-
);
|
|
357
|
-
ensureFileWithDefault(
|
|
358
|
-
item.paths.stateMap,
|
|
359
|
-
`# State Map\n\n- TODO: 补充状态与交互映射。\n`
|
|
360
|
-
);
|
|
361
|
-
ensureFileWithDefault(item.paths.raw, "{}\n");
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Writes generic cache skeleton files, then invokes optional lifecycle hook.
|
|
366
|
-
* @param {string} cacheKey
|
|
367
|
-
* @param {object} item
|
|
368
|
-
*/
|
|
369
|
-
function ensureEntryFilesAndHook(cacheKey, item) {
|
|
370
|
-
ensureEntryFiles(item);
|
|
371
|
-
runPostEnsureHook(cacheKey, item);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function validateIndex(index) {
|
|
375
|
-
const errors = [];
|
|
376
|
-
const normalized = normalizeIndexShape(index);
|
|
377
|
-
const keys = Object.keys(normalized.items || {});
|
|
378
|
-
keys.forEach((cacheKey) => {
|
|
379
|
-
const item = normalized.items[cacheKey];
|
|
380
|
-
const required = [
|
|
381
|
-
"fileKey",
|
|
382
|
-
"scope",
|
|
383
|
-
"url",
|
|
384
|
-
"originalUrls",
|
|
385
|
-
"normalizationVersion",
|
|
386
|
-
"paths",
|
|
387
|
-
"syncedAt",
|
|
388
|
-
"completeness",
|
|
389
|
-
];
|
|
390
|
-
required.forEach((field) => {
|
|
391
|
-
if (item[field] === undefined || item[field] === null) {
|
|
392
|
-
errors.push(`${cacheKey}: 缺少字段 ${field}`);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
if (item.scope === "node" && !item.nodeId) {
|
|
396
|
-
errors.push(`${cacheKey}: node 作用域必须包含 nodeId`);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const flowKeys = Object.keys(normalized.flows || {});
|
|
401
|
-
flowKeys.forEach((flowId) => {
|
|
402
|
-
const flow = normalized.flows[flowId];
|
|
403
|
-
if (!flow || typeof flow !== "object") {
|
|
404
|
-
errors.push(`flow ${flowId}: 非法结构`);
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
if (!flow.id || flow.id !== flowId) {
|
|
408
|
-
errors.push(`flow ${flowId}: id 字段缺失或不一致`);
|
|
409
|
-
}
|
|
410
|
-
if (!Array.isArray(flow.nodes)) {
|
|
411
|
-
errors.push(`flow ${flowId}: nodes 必须是数组`);
|
|
412
|
-
}
|
|
413
|
-
if (!Array.isArray(flow.edges)) {
|
|
414
|
-
errors.push(`flow ${flowId}: edges 必须是数组`);
|
|
415
|
-
}
|
|
416
|
-
if (Array.isArray(flow.edges)) {
|
|
417
|
-
flow.edges.forEach((edge, idx) => {
|
|
418
|
-
if (!edge || typeof edge !== "object") {
|
|
419
|
-
errors.push(`flow ${flowId}: edge[${idx}] 非法`);
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
if (!edge.from || !edge.to) {
|
|
423
|
-
errors.push(`flow ${flowId}: edge[${idx}] 缺少 from/to`);
|
|
424
|
-
}
|
|
425
|
-
if (!edge.type) {
|
|
426
|
-
errors.push(`flow ${flowId}: edge[${idx}] 缺少 type`);
|
|
427
|
-
}
|
|
428
|
-
if (edge.from && !normalized.items[edge.from]) {
|
|
429
|
-
errors.push(`flow ${flowId}: edge[${idx}] from 不存在于 items: ${edge.from}`);
|
|
430
|
-
}
|
|
431
|
-
if (edge.to && !normalized.items[edge.to]) {
|
|
432
|
-
errors.push(`flow ${flowId}: edge[${idx}] to 不存在于 items: ${edge.to}`);
|
|
433
|
-
}
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
if (Array.isArray(flow.nodes)) {
|
|
437
|
-
flow.nodes.forEach((cacheKey) => {
|
|
438
|
-
if (!normalized.items[cacheKey]) {
|
|
439
|
-
errors.push(`flow ${flowId}: nodes 引用不存在于 items: ${cacheKey}`);
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
return errors;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/** @param {boolean} force 为 true 时覆盖已存在文件 */
|
|
449
|
-
function copyCursorBootstrap(force) {
|
|
450
|
-
const pairs = [
|
|
451
|
-
{ from: path.join("rules", "01-figma-cache-core.mdc"), to: path.join(".cursor", "rules", "01-figma-cache-core.mdc") },
|
|
452
|
-
{ from: path.join("rules", "02-figma-stack-adapter.mdc"), to: path.join(".cursor", "rules", "02-figma-stack-adapter.mdc") },
|
|
453
|
-
{ from: path.join("rules", "figma-local-cache-first.mdc"), to: path.join(".cursor", "rules", "figma-local-cache-first.mdc") },
|
|
454
|
-
{
|
|
455
|
-
from: path.join("skills", "figma-mcp-local-cache", "SKILL.md"),
|
|
456
|
-
to: path.join(".cursor", "skills", "figma-mcp-local-cache", "SKILL.md"),
|
|
457
|
-
},
|
|
458
|
-
{ from: "figma-cache.config.example.js", to: "figma-cache.config.example.js" },
|
|
459
|
-
];
|
|
460
|
-
if (!fs.existsSync(CURSOR_BOOTSTRAP_DIR)) {
|
|
461
|
-
console.error(
|
|
462
|
-
`[figma-cache] cursor-bootstrap not found at ${normalizeSlash(CURSOR_BOOTSTRAP_DIR)} (broken package install?)`
|
|
463
|
-
);
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
let copied = 0;
|
|
467
|
-
let skipped = 0;
|
|
468
|
-
pairs.forEach(({ from: relFrom, to: relTo }) => {
|
|
469
|
-
const absFrom = path.join(CURSOR_BOOTSTRAP_DIR, relFrom);
|
|
470
|
-
const absTo = path.join(ROOT, relTo);
|
|
471
|
-
if (!fs.existsSync(absFrom)) {
|
|
472
|
-
console.error(`[figma-cache] missing template file: ${normalizeSlash(absFrom)}`);
|
|
473
|
-
process.exit(1);
|
|
474
|
-
}
|
|
475
|
-
fs.mkdirSync(path.dirname(absTo), { recursive: true });
|
|
476
|
-
if (fs.existsSync(absTo) && !force) {
|
|
477
|
-
skipped += 1;
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
fs.copyFileSync(absFrom, absTo);
|
|
481
|
-
copied += 1;
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
const agentSrc = path.join(CURSOR_BOOTSTRAP_DIR, "AGENT-SETUP-PROMPT.md");
|
|
485
|
-
const agentDest = path.join(ROOT, "AGENT-SETUP-PROMPT.md");
|
|
486
|
-
if (!fs.existsSync(agentSrc)) {
|
|
487
|
-
console.error(`[figma-cache] missing ${normalizeSlash(agentSrc)}`);
|
|
488
|
-
process.exit(1);
|
|
489
|
-
}
|
|
490
|
-
let agentBody = fs.readFileSync(agentSrc, "utf8");
|
|
491
|
-
const npmPkg = readSelfNpmPackageName();
|
|
492
|
-
agentBody = agentBody.replace(/\{\{NPM_PACKAGE_NAME\}\}/g, npmPkg);
|
|
493
|
-
fs.writeFileSync(agentDest, agentBody, "utf8");
|
|
494
|
-
|
|
495
|
-
const colleagueSrc = path.join(__dirname, "colleague-guide-zh.md");
|
|
496
|
-
const colleagueDest = path.join(CACHE_DIR, "colleague-guide-zh.md");
|
|
497
|
-
if (!fs.existsSync(colleagueSrc)) {
|
|
498
|
-
console.error(`[figma-cache] missing ${normalizeSlash(colleagueSrc)} (broken package install?)`);
|
|
499
|
-
process.exit(1);
|
|
500
|
-
}
|
|
501
|
-
const colleagueSameFile = path.resolve(colleagueSrc) === path.resolve(colleagueDest);
|
|
502
|
-
if (!colleagueSameFile) {
|
|
503
|
-
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
504
|
-
fs.copyFileSync(colleagueSrc, colleagueDest);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
console.log(
|
|
508
|
-
JSON.stringify(
|
|
509
|
-
{
|
|
510
|
-
ok: true,
|
|
511
|
-
root: normalizeSlash(ROOT),
|
|
512
|
-
copied,
|
|
513
|
-
skipped,
|
|
514
|
-
force: !!force,
|
|
515
|
-
hint: skipped
|
|
516
|
-
? "Some template files were skipped (already exist). Re-run with --force to overwrite."
|
|
517
|
-
: "Done.",
|
|
518
|
-
agentPromptFile: normalizeSlash(agentDest),
|
|
519
|
-
colleagueGuideFile: normalizeSlash(colleagueDest),
|
|
520
|
-
colleagueGuideSynced: !colleagueSameFile,
|
|
521
|
-
colleagueGuideNote: colleagueSameFile
|
|
522
|
-
? "colleague-guide-zh.md already at package path (toolchain dev tree); no copy."
|
|
523
|
-
: "colleague-guide-zh.md refreshed under FIGMA_CACHE_DIR (default figma-cache/).",
|
|
524
|
-
agentPromptNote:
|
|
525
|
-
"AGENT-SETUP-PROMPT.md is refreshed every run. Next: @ it in Cursor; after Agent finishes, run npm run figma:cache:init (or npx figma-cache init if scripts are missing).",
|
|
526
|
-
npmPackageName: npmPkg,
|
|
527
|
-
},
|
|
528
|
-
null,
|
|
529
|
-
2
|
|
530
|
-
)
|
|
531
|
-
);
|
|
532
|
-
console.log(
|
|
533
|
-
"\n" +
|
|
534
|
-
"================================================================\n" +
|
|
535
|
-
"下一步(请按顺序):\n" +
|
|
536
|
-
"1) 在 Cursor 对话中输入 @AGENT-SETUP-PROMPT.md,并说明「按该文档执行」\n" +
|
|
537
|
-
" (每次 cursor init 都会刷新该文件;无需再整篇粘贴。)\n" +
|
|
538
|
-
"2) 待 Agent 完成后,在项目根初始化本地缓存索引:\n" +
|
|
539
|
-
" npm run figma:cache:init\n" +
|
|
540
|
-
" 若尚未补全 npm scripts,请改用:npx figma-cache init\n" +
|
|
541
|
-
"================================================================\n"
|
|
542
|
-
);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function collectMarkdownFiles(dir) {
|
|
546
|
-
if (!fs.existsSync(dir)) {
|
|
157
|
+
function normalizeCompletenessList(input) {
|
|
158
|
+
if (!Array.isArray(input)) {
|
|
547
159
|
return [];
|
|
548
160
|
}
|
|
161
|
+
const seen = new Set();
|
|
549
162
|
const output = [];
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (entry.isDirectory()) {
|
|
554
|
-
output.push(...collectMarkdownFiles(fullPath));
|
|
163
|
+
input.forEach((entry) => {
|
|
164
|
+
const value = String(entry || "").trim();
|
|
165
|
+
if (!value || seen.has(value)) {
|
|
555
166
|
return;
|
|
556
167
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
168
|
+
seen.add(value);
|
|
169
|
+
output.push(value);
|
|
560
170
|
});
|
|
561
171
|
return output;
|
|
562
172
|
}
|
|
563
173
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
574
|
-
const urls = extractFigmaUrls(content);
|
|
575
|
-
urls.forEach((url) => {
|
|
576
|
-
try {
|
|
577
|
-
upsertByUrl(url, { source: "backfill" });
|
|
578
|
-
hit += 1;
|
|
579
|
-
} catch (error) {
|
|
580
|
-
// 忽略无法解析的 URL
|
|
581
|
-
}
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
console.log(`Backfill done. scanned files=${files.length}, urls=${hit}`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function slugifyFlowId(name) {
|
|
588
|
-
const raw = String(name || "")
|
|
589
|
-
.trim()
|
|
590
|
-
.toLowerCase()
|
|
591
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
592
|
-
.replace(/^-+|-+$/g, "");
|
|
593
|
-
return raw || `flow-${Date.now()}`;
|
|
594
|
-
}
|
|
174
|
+
const entryFilesService = createEntryFilesService({
|
|
175
|
+
fs,
|
|
176
|
+
path,
|
|
177
|
+
resolveMaybeAbsolutePath,
|
|
178
|
+
normalizeCompletenessList,
|
|
179
|
+
completenessAllDimensions: COMPLETENESS_ALL_DIMENSIONS,
|
|
180
|
+
runPostEnsureHook,
|
|
181
|
+
});
|
|
182
|
+
const { ensureEntryFilesAndHook } = entryFilesService;
|
|
595
183
|
|
|
596
|
-
function
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
title: meta && meta.title ? meta.title : flowId,
|
|
603
|
-
description: meta && meta.description ? meta.description : "",
|
|
604
|
-
createdAt: new Date().toISOString(),
|
|
605
|
-
updatedAt: new Date().toISOString(),
|
|
606
|
-
nodes: [],
|
|
607
|
-
edges: [],
|
|
608
|
-
assumptions: [],
|
|
609
|
-
openQuestions: [],
|
|
184
|
+
function parseCompletenessFromArgs(args) {
|
|
185
|
+
const completenessArg = args.find((x) => x.startsWith("--completeness="));
|
|
186
|
+
if (!completenessArg) {
|
|
187
|
+
return {
|
|
188
|
+
completeness: [...DEFAULT_COMPLETENESS],
|
|
189
|
+
fromCliArg: false,
|
|
610
190
|
};
|
|
611
191
|
}
|
|
612
|
-
return
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if (!flow.nodes.includes(cacheKey)) {
|
|
618
|
-
flow.nodes.push(cacheKey);
|
|
619
|
-
}
|
|
620
|
-
flow.updatedAt = new Date().toISOString();
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
function addFlowEdge(index, flowId, fromKey, toKey, type, note) {
|
|
624
|
-
const flow = ensureFlow(index, flowId, {});
|
|
625
|
-
const edge = {
|
|
626
|
-
id: `${fromKey}->${toKey}:${type}:${Date.now()}`,
|
|
627
|
-
from: fromKey,
|
|
628
|
-
to: toKey,
|
|
629
|
-
type,
|
|
630
|
-
note: note || "",
|
|
631
|
-
createdAt: new Date().toISOString(),
|
|
192
|
+
return {
|
|
193
|
+
completeness: normalizeCompletenessList(
|
|
194
|
+
completenessArg.split("=").slice(1).join("=").split(","),
|
|
195
|
+
),
|
|
196
|
+
fromCliArg: true,
|
|
632
197
|
};
|
|
633
|
-
flow.edges.push(edge);
|
|
634
|
-
flow.updatedAt = new Date().toISOString();
|
|
635
198
|
}
|
|
636
199
|
|
|
637
200
|
/** @returns {string} */
|
|
@@ -685,137 +248,112 @@ function safeFileSize(absPath) {
|
|
|
685
248
|
}
|
|
686
249
|
}
|
|
687
250
|
|
|
688
|
-
function
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
251
|
+
function runUpsertLikeCommand(commandName, args, shouldEnsureFiles) {
|
|
252
|
+
const url = args[0];
|
|
253
|
+
const sourceArg = args.find((x) => x.startsWith("--source="));
|
|
254
|
+
const source = sourceArg ? sourceArg.split("=")[1] : "manual";
|
|
255
|
+
const allowSkeletonWithFigmaMcp = args.includes(
|
|
256
|
+
"--allow-skeleton-with-figma-mcp",
|
|
257
|
+
);
|
|
258
|
+
const { completeness } = parseCompletenessFromArgs(args);
|
|
259
|
+
|
|
260
|
+
const preview = previewUpsertByUrl(url, { source, completeness });
|
|
261
|
+
if (source === "figma-mcp") {
|
|
262
|
+
const mcpErrors = validateMcpRawEvidence(
|
|
263
|
+
preview.normalized.cacheKey,
|
|
264
|
+
preview.item,
|
|
265
|
+
completeness,
|
|
266
|
+
{ allowSkeletonWithFigmaMcp },
|
|
267
|
+
{
|
|
268
|
+
fs,
|
|
269
|
+
path,
|
|
270
|
+
resolveMaybeAbsolutePath,
|
|
271
|
+
safeReadJson,
|
|
272
|
+
normalizeSlash,
|
|
273
|
+
normalizeCompletenessList,
|
|
274
|
+
completenessToolRequirements: COMPLETENESS_TOOL_REQUIREMENTS,
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
if (mcpErrors.length) {
|
|
278
|
+
console.error(
|
|
279
|
+
`${commandName} failed: source=figma-mcp but MCP raw evidence is incomplete`,
|
|
280
|
+
);
|
|
281
|
+
mcpErrors.forEach((err) => console.error(`- ${err}`));
|
|
282
|
+
process.exit(2);
|
|
704
283
|
}
|
|
705
|
-
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
const entries = filteredKeys.map((cacheKey) => {
|
|
709
|
-
const item = items[cacheKey];
|
|
710
|
-
const metaAbs = resolveMaybeAbsolutePath(item.paths.meta);
|
|
711
|
-
const nodeDir = path.dirname(metaAbs);
|
|
712
|
-
const mcpRawDir = path.join(nodeDir, "mcp-raw");
|
|
713
|
-
const manifestAbs = path.join(mcpRawDir, "mcp-raw-manifest.json");
|
|
714
|
-
const manifest = safeReadJson(manifestAbs);
|
|
715
|
-
const filesMap = manifest && manifest.files && typeof manifest.files === "object" ? manifest.files : {};
|
|
716
|
-
const fileEntries = Object.entries(filesMap);
|
|
717
|
-
const mcpRawBytes = fileEntries.reduce((acc, [, fileName]) => {
|
|
718
|
-
const abs = path.join(mcpRawDir, String(fileName));
|
|
719
|
-
return acc + safeFileSize(abs);
|
|
720
|
-
}, 0);
|
|
721
|
-
const mcpRawFilesCount = fileEntries.length;
|
|
722
|
-
const toolCalls = manifest && manifest.toolCalls && typeof manifest.toolCalls === "object" ? manifest.toolCalls : {};
|
|
723
|
-
const toolCallCount = Object.values(toolCalls).reduce((acc, v) => {
|
|
724
|
-
const count = v && typeof v === "object" ? Number(v.count) : 0;
|
|
725
|
-
return acc + (Number.isFinite(count) ? count : 0);
|
|
726
|
-
}, 0);
|
|
727
|
-
const designContextFile =
|
|
728
|
-
filesMap && Object.prototype.hasOwnProperty.call(filesMap, "get_design_context")
|
|
729
|
-
? String(filesMap.get_design_context)
|
|
730
|
-
: "";
|
|
731
|
-
const designContextChars = designContextFile
|
|
732
|
-
? safeFileSize(path.join(mcpRawDir, designContextFile))
|
|
733
|
-
: 0;
|
|
284
|
+
}
|
|
734
285
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
286
|
+
const result = upsertByUrl(url, { source, completeness });
|
|
287
|
+
if (shouldEnsureFiles) {
|
|
288
|
+
ensureEntryFilesAndHook(result.normalized.cacheKey, result.item);
|
|
289
|
+
console.log(
|
|
290
|
+
JSON.stringify(
|
|
291
|
+
{
|
|
292
|
+
cacheKey: result.normalized.cacheKey,
|
|
293
|
+
ensured: true,
|
|
294
|
+
paths: result.item.paths,
|
|
295
|
+
},
|
|
296
|
+
null,
|
|
297
|
+
2,
|
|
298
|
+
),
|
|
299
|
+
);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
748
302
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
return acc;
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
nodes: 0,
|
|
763
|
-
nodesWithMcpRaw: 0,
|
|
764
|
-
mcpRawBytes: 0,
|
|
765
|
-
tokenProxyBytes: 0,
|
|
766
|
-
toolCallCount: 0,
|
|
767
|
-
}
|
|
303
|
+
console.log(
|
|
304
|
+
JSON.stringify(
|
|
305
|
+
{
|
|
306
|
+
cacheKey: result.normalized.cacheKey,
|
|
307
|
+
scope: result.item.scope,
|
|
308
|
+
syncedAt: result.item.syncedAt,
|
|
309
|
+
},
|
|
310
|
+
null,
|
|
311
|
+
2,
|
|
312
|
+
),
|
|
768
313
|
);
|
|
769
|
-
|
|
770
|
-
totals.tokenProxyChars = totals.tokenProxyBytes;
|
|
771
|
-
|
|
772
|
-
return {
|
|
773
|
-
generatedAt: new Date().toISOString(),
|
|
774
|
-
filters: {
|
|
775
|
-
cacheKey: options.cacheKey || null,
|
|
776
|
-
mcpOnly: !!options.mcpOnly,
|
|
777
|
-
limit,
|
|
778
|
-
},
|
|
779
|
-
totals,
|
|
780
|
-
entries: limitedEntries,
|
|
781
|
-
};
|
|
782
314
|
}
|
|
783
|
-
|
|
784
315
|
function run() {
|
|
785
316
|
const [, , cmd, ...args] = process.argv;
|
|
786
317
|
if (!cmd) {
|
|
787
318
|
const ex = inferCliExample();
|
|
319
|
+
const defaultCompletenessText = DEFAULT_COMPLETENESS.join(",");
|
|
788
320
|
console.log("Usage:");
|
|
789
|
-
console.log(
|
|
321
|
+
console.log(
|
|
322
|
+
` (invoke examples: ${ex} | node bin/figma-cache.js | node figma-cache/figma-cache.js)`,
|
|
323
|
+
);
|
|
790
324
|
console.log(` ${ex} normalize <figmaUrl>`);
|
|
791
325
|
console.log(` ${ex} get <figmaUrl>`);
|
|
792
|
-
console.log(
|
|
326
|
+
console.log(
|
|
327
|
+
` ${ex} upsert <figmaUrl> [--source=manual] [--completeness=a,b] [--allow-skeleton-with-figma-mcp] (default completeness=${defaultCompletenessText})`,
|
|
328
|
+
);
|
|
793
329
|
console.log(` ${ex} validate`);
|
|
794
330
|
console.log(` ${ex} stale [--days=14]`);
|
|
795
331
|
console.log(` ${ex} backfill`);
|
|
796
|
-
console.log(` ${ex} budget [--mcp-only] [--cacheKey=<fileKey#nodeId>] [--limit=50]`);
|
|
797
332
|
console.log(
|
|
798
|
-
` ${ex}
|
|
333
|
+
` ${ex} budget [--mcp-only] [--cacheKey=<fileKey#nodeId>] [--limit=50]`,
|
|
334
|
+
);
|
|
335
|
+
console.log(
|
|
336
|
+
` ${ex} ensure <figmaUrl> [--source=manual] [--completeness=a,b] [--allow-skeleton-with-figma-mcp] (default completeness=${defaultCompletenessText})`,
|
|
799
337
|
);
|
|
800
338
|
console.log(` ${ex} init`);
|
|
801
339
|
console.log(` ${ex} config`);
|
|
802
340
|
console.log(
|
|
803
|
-
" (optional) figma-cache.config.js | .figmacacherc.js | FIGMA_CACHE_PROJECT_CONFIG
|
|
341
|
+
" (optional) figma-cache.config.js | .figmacacherc.js | FIGMA_CACHE_PROJECT_CONFIG 鈥?hooks.postEnsure after ensure",
|
|
804
342
|
);
|
|
805
343
|
console.log(` ${ex} flow init --id=<flowId> [--title=...]`);
|
|
806
344
|
console.log(
|
|
807
|
-
`${ex} flow add-node --flow=<flowId> <figmaUrl> [--ensure] [--source=manual] [--completeness=a,b]
|
|
345
|
+
`${ex} flow add-node --flow=<flowId> <figmaUrl> [--ensure] [--source=manual] [--completeness=a,b]`,
|
|
808
346
|
);
|
|
809
347
|
console.log(
|
|
810
|
-
`${ex} flow link --flow=<flowId> <fromUrl> <toUrl> --type=next_step [--note=...]
|
|
348
|
+
`${ex} flow link --flow=<flowId> <fromUrl> <toUrl> --type=next_step [--note=...]`,
|
|
811
349
|
);
|
|
812
350
|
console.log(
|
|
813
|
-
`${ex} flow chain --flow=<flowId> <url1> <url2> ... [--type=next_step|related]
|
|
351
|
+
`${ex} flow chain --flow=<flowId> <url1> <url2> ... [--type=next_step|related]`,
|
|
814
352
|
);
|
|
815
353
|
console.log(` ${ex} flow show --flow=<flowId>`);
|
|
816
354
|
console.log(` ${ex} flow mermaid --flow=<flowId>`);
|
|
817
355
|
console.log(
|
|
818
|
-
`${ex} cursor init [--force] # copy .cursor templates + figma-cache.config.
|
|
356
|
+
`${ex} cursor init [--force] # copy .cursor templates + ensure figma-cache.config.js (+ cleanup safe legacy example) + refresh AGENT-SETUP-PROMPT.md + sync colleague-guide-zh.md into FIGMA_CACHE_DIR`,
|
|
819
357
|
);
|
|
820
358
|
process.exit(1);
|
|
821
359
|
}
|
|
@@ -827,7 +365,16 @@ function run() {
|
|
|
827
365
|
process.exit(1);
|
|
828
366
|
}
|
|
829
367
|
const force = args.includes("--force");
|
|
830
|
-
copyCursorBootstrap(force
|
|
368
|
+
copyCursorBootstrap(force, {
|
|
369
|
+
fs,
|
|
370
|
+
path,
|
|
371
|
+
ROOT,
|
|
372
|
+
CACHE_DIR,
|
|
373
|
+
CURSOR_BOOTSTRAP_DIR,
|
|
374
|
+
normalizeSlash,
|
|
375
|
+
readSelfNpmPackageName,
|
|
376
|
+
packageDir: __dirname,
|
|
377
|
+
});
|
|
831
378
|
return;
|
|
832
379
|
}
|
|
833
380
|
|
|
@@ -851,78 +398,33 @@ function run() {
|
|
|
851
398
|
item: item || null,
|
|
852
399
|
},
|
|
853
400
|
null,
|
|
854
|
-
2
|
|
855
|
-
)
|
|
401
|
+
2,
|
|
402
|
+
),
|
|
856
403
|
);
|
|
857
404
|
return;
|
|
858
405
|
}
|
|
859
406
|
|
|
860
407
|
if (cmd === "upsert") {
|
|
861
|
-
|
|
862
|
-
const sourceArg = args.find((x) => x.startsWith("--source="));
|
|
863
|
-
const completenessArg = args.find((x) => x.startsWith("--completeness="));
|
|
864
|
-
const source = sourceArg ? sourceArg.split("=")[1] : "manual";
|
|
865
|
-
const completeness = completenessArg
|
|
866
|
-
? completenessArg
|
|
867
|
-
.split("=")[1]
|
|
868
|
-
.split(",")
|
|
869
|
-
.map((x) => x.trim())
|
|
870
|
-
.filter(Boolean)
|
|
871
|
-
: [];
|
|
872
|
-
const result = upsertByUrl(url, { source, completeness });
|
|
873
|
-
console.log(
|
|
874
|
-
JSON.stringify(
|
|
875
|
-
{ cacheKey: result.normalized.cacheKey, scope: result.item.scope, syncedAt: result.item.syncedAt },
|
|
876
|
-
null,
|
|
877
|
-
2
|
|
878
|
-
)
|
|
879
|
-
);
|
|
408
|
+
runUpsertLikeCommand("upsert", args, false);
|
|
880
409
|
return;
|
|
881
410
|
}
|
|
882
411
|
|
|
883
412
|
if (cmd === "ensure") {
|
|
884
|
-
|
|
885
|
-
const sourceArg = args.find((x) => x.startsWith("--source="));
|
|
886
|
-
const completenessArg = args.find((x) => x.startsWith("--completeness="));
|
|
887
|
-
const allowSkeletonWithFigmaMcp = args.includes("--allow-skeleton-with-figma-mcp");
|
|
888
|
-
const source = sourceArg ? sourceArg.split("=")[1] : "manual";
|
|
889
|
-
const completeness = completenessArg
|
|
890
|
-
? completenessArg
|
|
891
|
-
.split("=")[1]
|
|
892
|
-
.split(",")
|
|
893
|
-
.map((x) => x.trim())
|
|
894
|
-
.filter(Boolean)
|
|
895
|
-
: [];
|
|
896
|
-
if (source === "figma-mcp" && !allowSkeletonWithFigmaMcp) {
|
|
897
|
-
console.error(
|
|
898
|
-
[
|
|
899
|
-
"ensure 检测到 --source=figma-mcp,但当前 CLI 不会主动发起 MCP 请求。",
|
|
900
|
-
"为避免“假成功”,已阻止本次写入。",
|
|
901
|
-
"请先由 Agent/Figma MCP 拉取并写入 mcp-raw,再执行 upsert/ensure;",
|
|
902
|
-
"若你只想生成本地骨架,请显式追加 --allow-skeleton-with-figma-mcp。",
|
|
903
|
-
].join("\n")
|
|
904
|
-
);
|
|
905
|
-
process.exit(2);
|
|
906
|
-
}
|
|
907
|
-
const result = upsertByUrl(url, { source, completeness });
|
|
908
|
-
ensureEntryFilesAndHook(result.normalized.cacheKey, result.item);
|
|
909
|
-
console.log(
|
|
910
|
-
JSON.stringify(
|
|
911
|
-
{
|
|
912
|
-
cacheKey: result.normalized.cacheKey,
|
|
913
|
-
ensured: true,
|
|
914
|
-
paths: result.item.paths,
|
|
915
|
-
},
|
|
916
|
-
null,
|
|
917
|
-
2
|
|
918
|
-
)
|
|
919
|
-
);
|
|
413
|
+
runUpsertLikeCommand("ensure", args, true);
|
|
920
414
|
return;
|
|
921
415
|
}
|
|
922
|
-
|
|
923
416
|
if (cmd === "validate") {
|
|
924
417
|
const index = readIndex();
|
|
925
|
-
const errors = validateIndex(index
|
|
418
|
+
const errors = validateIndex(index, {
|
|
419
|
+
fs,
|
|
420
|
+
path,
|
|
421
|
+
normalizeIndexShape,
|
|
422
|
+
normalizeCompletenessList,
|
|
423
|
+
resolveMaybeAbsolutePath,
|
|
424
|
+
safeReadJson,
|
|
425
|
+
normalizeSlash,
|
|
426
|
+
completenessToolRequirements: COMPLETENESS_TOOL_REQUIREMENTS,
|
|
427
|
+
});
|
|
926
428
|
if (!errors.length) {
|
|
927
429
|
console.log("Validation passed.");
|
|
928
430
|
return;
|
|
@@ -940,7 +442,14 @@ function run() {
|
|
|
940
442
|
}
|
|
941
443
|
|
|
942
444
|
if (cmd === "backfill") {
|
|
943
|
-
backfillFromIterations(
|
|
445
|
+
backfillFromIterations(
|
|
446
|
+
{ iterationsDir: ITERATIONS_DIR },
|
|
447
|
+
{
|
|
448
|
+
fs,
|
|
449
|
+
path,
|
|
450
|
+
upsertByUrl,
|
|
451
|
+
},
|
|
452
|
+
);
|
|
944
453
|
return;
|
|
945
454
|
}
|
|
946
455
|
|
|
@@ -948,9 +457,21 @@ function run() {
|
|
|
948
457
|
const mcpOnly = args.includes("--mcp-only");
|
|
949
458
|
const cacheKeyArg = args.find((x) => x.startsWith("--cacheKey="));
|
|
950
459
|
const limitArg = args.find((x) => x.startsWith("--limit="));
|
|
951
|
-
const cacheKey = cacheKeyArg
|
|
460
|
+
const cacheKey = cacheKeyArg
|
|
461
|
+
? cacheKeyArg.split("=").slice(1).join("=")
|
|
462
|
+
: "";
|
|
952
463
|
const limit = limitArg ? limitArg.split("=")[1] : "";
|
|
953
|
-
const report = buildBudgetReport(
|
|
464
|
+
const report = buildBudgetReport(
|
|
465
|
+
{ mcpOnly, cacheKey, limit },
|
|
466
|
+
{
|
|
467
|
+
path,
|
|
468
|
+
normalizeIndexShape,
|
|
469
|
+
readIndex,
|
|
470
|
+
resolveMaybeAbsolutePath,
|
|
471
|
+
safeReadJson,
|
|
472
|
+
safeFileSize,
|
|
473
|
+
},
|
|
474
|
+
);
|
|
954
475
|
console.log(JSON.stringify(report, null, 2));
|
|
955
476
|
return;
|
|
956
477
|
}
|
|
@@ -967,17 +488,16 @@ function run() {
|
|
|
967
488
|
iterationsDir: normalizeSlash(ITERATIONS_DIR),
|
|
968
489
|
staleDays: DEFAULT_STALE_DAYS,
|
|
969
490
|
defaultFlowId: DEFAULT_FLOW_ID || null,
|
|
491
|
+
defaultCompleteness: [...DEFAULT_COMPLETENESS],
|
|
970
492
|
normalizationVersion: NORMALIZATION_VERSION,
|
|
971
|
-
projectConfigPath:
|
|
972
|
-
? normalizeSlash(memoProjectConfigPath)
|
|
973
|
-
: null,
|
|
493
|
+
projectConfigPath: getProjectConfigPath(),
|
|
974
494
|
hooks: {
|
|
975
495
|
postEnsure: !!(hooks && typeof hooks.postEnsure === "function"),
|
|
976
496
|
},
|
|
977
497
|
},
|
|
978
498
|
null,
|
|
979
|
-
2
|
|
980
|
-
)
|
|
499
|
+
2,
|
|
500
|
+
),
|
|
981
501
|
);
|
|
982
502
|
return;
|
|
983
503
|
}
|
|
@@ -993,8 +513,8 @@ function run() {
|
|
|
993
513
|
indexPath: normalizeSlash(INDEX_PATH),
|
|
994
514
|
},
|
|
995
515
|
null,
|
|
996
|
-
2
|
|
997
|
-
)
|
|
516
|
+
2,
|
|
517
|
+
),
|
|
998
518
|
);
|
|
999
519
|
return;
|
|
1000
520
|
}
|
|
@@ -1006,176 +526,25 @@ function run() {
|
|
|
1006
526
|
indexPath: normalizeSlash(INDEX_PATH),
|
|
1007
527
|
},
|
|
1008
528
|
null,
|
|
1009
|
-
2
|
|
1010
|
-
)
|
|
529
|
+
2,
|
|
530
|
+
),
|
|
1011
531
|
);
|
|
1012
532
|
return;
|
|
1013
533
|
}
|
|
1014
534
|
|
|
1015
535
|
if (cmd === "flow") {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const title = titleArg ? titleArg.split("=").slice(1).join("=") : flowId;
|
|
1029
|
-
const description = descArg ? descArg.split("=").slice(1).join("=") : "";
|
|
1030
|
-
const index = normalizeIndexShape(readIndex());
|
|
1031
|
-
ensureFlow(index, flowId, { title, description });
|
|
1032
|
-
writeIndex(index);
|
|
1033
|
-
console.log(JSON.stringify({ flowId, created: true }, null, 2));
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
if (sub === "add-node") {
|
|
1038
|
-
const flowId = resolveFlowIdFromArgs(rest);
|
|
1039
|
-
if (!flowId) {
|
|
1040
|
-
console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
|
|
1041
|
-
process.exit(1);
|
|
1042
|
-
}
|
|
1043
|
-
const url = rest.find((x) => !x.startsWith("--"));
|
|
1044
|
-
const ensureArg = rest.includes("--ensure");
|
|
1045
|
-
const sourceArg = rest.find((x) => x.startsWith("--source="));
|
|
1046
|
-
const completenessArg = rest.find((x) => x.startsWith("--completeness="));
|
|
1047
|
-
const source = sourceArg ? sourceArg.split("=")[1] : "manual";
|
|
1048
|
-
const completeness = completenessArg
|
|
1049
|
-
? completenessArg
|
|
1050
|
-
.split("=")[1]
|
|
1051
|
-
.split(",")
|
|
1052
|
-
.map((x) => x.trim())
|
|
1053
|
-
.filter(Boolean)
|
|
1054
|
-
: [];
|
|
1055
|
-
const index = normalizeIndexShape(readIndex());
|
|
1056
|
-
const normalized = normalizeFigmaUrl(url);
|
|
1057
|
-
if (!ensureArg && !getItem(index, normalized.cacheKey)) {
|
|
1058
|
-
console.error(
|
|
1059
|
-
`Missing cache item for ${normalized.cacheKey}. Run figma:cache:ensure first, or pass --ensure.`
|
|
1060
|
-
);
|
|
1061
|
-
process.exit(2);
|
|
1062
|
-
}
|
|
1063
|
-
if (ensureArg) {
|
|
1064
|
-
upsertByUrl(url, { source, completeness });
|
|
1065
|
-
const refreshed = normalizeIndexShape(readIndex());
|
|
1066
|
-
const item = getItem(refreshed, normalized.cacheKey);
|
|
1067
|
-
if (item) {
|
|
1068
|
-
ensureEntryFilesAndHook(normalized.cacheKey, item);
|
|
1069
|
-
}
|
|
1070
|
-
Object.assign(index, refreshed);
|
|
1071
|
-
}
|
|
1072
|
-
upsertFlowNode(index, flowId, normalized.cacheKey);
|
|
1073
|
-
writeIndex(index);
|
|
1074
|
-
console.log(
|
|
1075
|
-
JSON.stringify(
|
|
1076
|
-
{ flowId, cacheKey: normalized.cacheKey, added: true, ensured: ensureArg },
|
|
1077
|
-
null,
|
|
1078
|
-
2
|
|
1079
|
-
)
|
|
1080
|
-
);
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
if (sub === "link") {
|
|
1085
|
-
const flowId = resolveFlowIdFromArgs(rest);
|
|
1086
|
-
const typeArg = rest.find((x) => x.startsWith("--type="));
|
|
1087
|
-
const noteArg = rest.find((x) => x.startsWith("--note="));
|
|
1088
|
-
const urls = rest.filter((x) => !x.startsWith("--"));
|
|
1089
|
-
if (!flowId) {
|
|
1090
|
-
console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
|
|
1091
|
-
process.exit(1);
|
|
1092
|
-
}
|
|
1093
|
-
if (urls.length < 2) {
|
|
1094
|
-
console.error("Missing <fromUrl> <toUrl>");
|
|
1095
|
-
process.exit(1);
|
|
1096
|
-
}
|
|
1097
|
-
const type = typeArg ? typeArg.split("=")[1] : "related";
|
|
1098
|
-
const note = noteArg ? noteArg.split("=").slice(1).join("=") : "";
|
|
1099
|
-
const from = normalizeFigmaUrl(urls[0]).cacheKey;
|
|
1100
|
-
const to = normalizeFigmaUrl(urls[1]).cacheKey;
|
|
1101
|
-
const index = normalizeIndexShape(readIndex());
|
|
1102
|
-
if (!getItem(index, from) || !getItem(index, to)) {
|
|
1103
|
-
console.error("Missing cache item for from/to. Cache urls first with ensure/upsert.");
|
|
1104
|
-
process.exit(2);
|
|
1105
|
-
}
|
|
1106
|
-
upsertFlowNode(index, flowId, from);
|
|
1107
|
-
upsertFlowNode(index, flowId, to);
|
|
1108
|
-
addFlowEdge(index, flowId, from, to, type, note);
|
|
1109
|
-
writeIndex(index);
|
|
1110
|
-
console.log(JSON.stringify({ flowId, from, to, type, linked: true }, null, 2));
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
if (sub === "chain") {
|
|
1115
|
-
const flowId = resolveFlowIdFromArgs(rest);
|
|
1116
|
-
const typeArg = rest.find((x) => x.startsWith("--type="));
|
|
1117
|
-
const type = typeArg ? typeArg.split("=")[1] : "related";
|
|
1118
|
-
const urls = rest.filter((x) => !x.startsWith("--"));
|
|
1119
|
-
if (!flowId) {
|
|
1120
|
-
console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
|
|
1121
|
-
process.exit(1);
|
|
1122
|
-
}
|
|
1123
|
-
if (urls.length < 2) {
|
|
1124
|
-
console.error("Need at least 2 urls");
|
|
1125
|
-
process.exit(1);
|
|
1126
|
-
}
|
|
1127
|
-
const index = normalizeIndexShape(readIndex());
|
|
1128
|
-
const keys = urls.map((u) => normalizeFigmaUrl(u).cacheKey);
|
|
1129
|
-
keys.forEach((k) => {
|
|
1130
|
-
if (!getItem(index, k)) {
|
|
1131
|
-
console.error(`Missing cache item for ${k}. Ensure each url is cached first.`);
|
|
1132
|
-
process.exit(2);
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
keys.forEach((k) => upsertFlowNode(index, flowId, k));
|
|
1136
|
-
for (let i = 0; i < keys.length - 1; i += 1) {
|
|
1137
|
-
addFlowEdge(index, flowId, keys[i], keys[i + 1], type, "");
|
|
1138
|
-
}
|
|
1139
|
-
writeIndex(index);
|
|
1140
|
-
console.log(JSON.stringify({ flowId, chained: keys.length - 1, type }, null, 2));
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
if (sub === "show") {
|
|
1145
|
-
const flowId = resolveFlowIdFromArgs(rest);
|
|
1146
|
-
if (!flowId) {
|
|
1147
|
-
console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
|
|
1148
|
-
process.exit(1);
|
|
1149
|
-
}
|
|
1150
|
-
const index = normalizeIndexShape(readIndex());
|
|
1151
|
-
const flow = index.flows[flowId];
|
|
1152
|
-
console.log(JSON.stringify({ flowId, flow: flow || null }, null, 2));
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
if (sub === "mermaid") {
|
|
1157
|
-
const flowId = resolveFlowIdFromArgs(rest);
|
|
1158
|
-
if (!flowId) {
|
|
1159
|
-
console.error("Missing --flow=<flowId> or env FIGMA_DEFAULT_FLOW");
|
|
1160
|
-
process.exit(1);
|
|
1161
|
-
}
|
|
1162
|
-
const index = normalizeIndexShape(readIndex());
|
|
1163
|
-
const flow = index.flows[flowId];
|
|
1164
|
-
if (!flow) {
|
|
1165
|
-
console.error(`Unknown flow: ${flowId}`);
|
|
1166
|
-
process.exit(1);
|
|
1167
|
-
}
|
|
1168
|
-
const lines = ["flowchart LR"];
|
|
1169
|
-
(flow.edges || []).forEach((edge) => {
|
|
1170
|
-
const label = edge.type || "edge";
|
|
1171
|
-
lines.push(` ${edge.from} -->|${label}| ${edge.to}`);
|
|
1172
|
-
});
|
|
1173
|
-
console.log(lines.join("\n"));
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
console.error(`Unknown flow subcommand: ${sub}`);
|
|
1178
|
-
process.exit(1);
|
|
536
|
+
handleFlowCommand(args, {
|
|
537
|
+
resolveFlowIdFromArgs,
|
|
538
|
+
parseCompletenessFromArgs,
|
|
539
|
+
normalizeIndexShape,
|
|
540
|
+
readIndex,
|
|
541
|
+
writeIndex,
|
|
542
|
+
normalizeFigmaUrl,
|
|
543
|
+
getItem,
|
|
544
|
+
upsertByUrl,
|
|
545
|
+
ensureEntryFilesAndHook,
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
1179
548
|
}
|
|
1180
549
|
|
|
1181
550
|
console.error(`Unknown command: ${cmd}`);
|