@nick848/ft 0.1.0 → 0.1.1
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 +42 -106
- package/cursor-commands-templates/ft-archive.md +11 -0
- package/cursor-commands-templates/ft-build.md +44 -0
- package/cursor-commands-templates/ft-design.md +11 -0
- package/cursor-commands-templates/ft-fill-context.md +13 -0
- package/cursor-commands-templates/ft-hotfix.md +33 -0
- package/cursor-commands-templates/ft-open.md +11 -0
- package/cursor-commands-templates/ft-sync-cursor-commands.md +11 -0
- package/cursor-commands-templates/ft-tweak.md +36 -0
- package/cursor-commands-templates/ft-verify.md +43 -0
- package/cursor-commands-templates/ft-visual-tools-install.md +26 -0
- package/dist/index.js +1109 -304
- package/dist/index.js.map +1 -1
- package/locales/en.json +95 -16
- package/locales/zh-CN.json +95 -16
- package/package.json +5 -4
- package/scripts/visual-diff.mjs +193 -0
- package/skills-templates/visual-fidelity/SKILL.md +77 -0
- package/skills-templates/visual-fidelity/checklist-schema.md +33 -0
- package/templates/AGENTS.md.en.hbs +5 -3
- package/templates/AGENTS.md.zh.hbs +5 -3
- package/templates/visual-spec.md +25 -0
- package/templates/visual-verify.json +14 -0
package/dist/index.js
CHANGED
|
@@ -5,11 +5,11 @@ import { Command } from "commander";
|
|
|
5
5
|
|
|
6
6
|
// src/cli/commands/init.ts
|
|
7
7
|
import chalk2 from "chalk";
|
|
8
|
-
import
|
|
9
|
-
import { execa as
|
|
8
|
+
import inquirer2 from "inquirer";
|
|
9
|
+
import { execa as execa3 } from "execa";
|
|
10
10
|
|
|
11
11
|
// src/core/detector.ts
|
|
12
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2
|
|
12
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
13
13
|
import path2 from "path";
|
|
14
14
|
import { execa } from "execa";
|
|
15
15
|
|
|
@@ -43,6 +43,15 @@ function getRulesTemplatesDir() {
|
|
|
43
43
|
function getLocalesDir() {
|
|
44
44
|
return path.join(getPackageRoot(), "locales");
|
|
45
45
|
}
|
|
46
|
+
function getCursorCommandsTemplatesDir() {
|
|
47
|
+
return path.join(getPackageRoot(), "cursor-commands-templates");
|
|
48
|
+
}
|
|
49
|
+
function getSkillsTemplatesDir() {
|
|
50
|
+
return path.join(getPackageRoot(), "skills-templates");
|
|
51
|
+
}
|
|
52
|
+
function getScriptsDir() {
|
|
53
|
+
return path.join(getPackageRoot(), "scripts");
|
|
54
|
+
}
|
|
46
55
|
|
|
47
56
|
// src/core/detector.ts
|
|
48
57
|
var KARPATHY_PATHS = [
|
|
@@ -70,14 +79,6 @@ function detectKarpathyRules(projectRoot = process.cwd()) {
|
|
|
70
79
|
const templatesDir = getRulesTemplatesDir();
|
|
71
80
|
return existsSync2(path2.join(templatesDir, "cursor.mdc"));
|
|
72
81
|
}
|
|
73
|
-
async function detectGitNexus() {
|
|
74
|
-
try {
|
|
75
|
-
const { stdout } = await execa("gitnexus", ["--version"]);
|
|
76
|
-
return stdout.trim();
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
82
|
function detectPlatformsInProject(projectRoot = process.cwd()) {
|
|
82
83
|
const found = [];
|
|
83
84
|
for (const [id, dir] of Object.entries(PLATFORM_DIRS)) {
|
|
@@ -135,6 +136,9 @@ function detectProjectMeta(projectRoot = process.cwd()) {
|
|
|
135
136
|
scripts
|
|
136
137
|
};
|
|
137
138
|
}
|
|
139
|
+
function isStdinTTY() {
|
|
140
|
+
return Boolean(process.stdin.isTTY);
|
|
141
|
+
}
|
|
138
142
|
function isStdoutTTY() {
|
|
139
143
|
return Boolean(process.stdout.isTTY);
|
|
140
144
|
}
|
|
@@ -144,19 +148,6 @@ function agentsMdHasPendingPlaceholders(projectRoot = process.cwd()) {
|
|
|
144
148
|
const content = readFileSync2(agentsPath, "utf-8");
|
|
145
149
|
return content.includes("[NEEDS LLM INPUT]");
|
|
146
150
|
}
|
|
147
|
-
function gitNexusGraphExists(projectRoot = process.cwd()) {
|
|
148
|
-
const graphDir = path2.join(projectRoot, ".gitnexus");
|
|
149
|
-
return existsSync2(graphDir);
|
|
150
|
-
}
|
|
151
|
-
function countGitNexusIndexFiles(projectRoot = process.cwd()) {
|
|
152
|
-
const graphDir = path2.join(projectRoot, ".gitnexus");
|
|
153
|
-
if (!existsSync2(graphDir)) return 0;
|
|
154
|
-
try {
|
|
155
|
-
return readdirSync(graphDir).length;
|
|
156
|
-
} catch {
|
|
157
|
-
return 0;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
151
|
|
|
161
152
|
// src/core/config.ts
|
|
162
153
|
import { cosmiconfig } from "cosmiconfig";
|
|
@@ -171,18 +162,18 @@ var DEFAULT_CONFIG = {
|
|
|
171
162
|
auto_trigger_lines: 500,
|
|
172
163
|
exclude_patterns: ["--json", "--format json", "--xml"]
|
|
173
164
|
},
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
165
|
+
figma: {
|
|
166
|
+
enabled: false,
|
|
167
|
+
configured: false
|
|
168
|
+
},
|
|
169
|
+
tools: ["cursor", "codex", "opencode"]
|
|
179
170
|
};
|
|
180
171
|
function mergeConfig(partial) {
|
|
181
172
|
return {
|
|
182
173
|
...DEFAULT_CONFIG,
|
|
183
174
|
...partial,
|
|
184
175
|
rtk: { ...DEFAULT_CONFIG.rtk, ...partial?.rtk },
|
|
185
|
-
|
|
176
|
+
figma: { ...DEFAULT_CONFIG.figma, ...partial?.figma },
|
|
186
177
|
tools: partial?.tools ?? DEFAULT_CONFIG.tools
|
|
187
178
|
};
|
|
188
179
|
}
|
|
@@ -226,13 +217,7 @@ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameS
|
|
|
226
217
|
import path4 from "path";
|
|
227
218
|
|
|
228
219
|
// src/core/template.ts
|
|
229
|
-
function
|
|
230
|
-
if (lang === "en") {
|
|
231
|
-
return "If GitNexus is installed in this project, AI **must prioritize** GitNexus MCP tools (e.g. `gitnexus_get_symbol`) for code scanning and dependency analysis. If not installed, ignore this rule.";
|
|
232
|
-
}
|
|
233
|
-
return "\u5982\u679C\u5F53\u524D\u5DE5\u7A0B\u5DF2\u5B89\u88C5 GitNexus\uFF0CAI \u5728\u8FDB\u884C\u4EE3\u7801\u626B\u63CF\u548C\u5206\u6790\u65F6**\u5FC5\u987B\u4F18\u5148\u4F7F\u7528** GitNexus MCP \u5DE5\u5177\uFF08\u5982 `gitnexus_get_symbol`\uFF09\u83B7\u53D6\u7B26\u53F7\u7EA7\u4F9D\u8D56\u5173\u7CFB\u3002\u82E5\u672A\u5B89\u88C5\uFF0C\u8BF7\u5FFD\u7565\u6B64\u6761\u3002";
|
|
234
|
-
}
|
|
235
|
-
function buildAgentsTemplateData(meta, lang) {
|
|
220
|
+
function buildAgentsTemplateData(meta) {
|
|
236
221
|
return {
|
|
237
222
|
projectName: meta.name,
|
|
238
223
|
packageManager: meta.packageManager,
|
|
@@ -240,8 +225,7 @@ function buildAgentsTemplateData(meta, lang) {
|
|
|
240
225
|
language: meta.language,
|
|
241
226
|
monorepo: meta.monorepo,
|
|
242
227
|
workspaces: meta.workspaces,
|
|
243
|
-
scripts: meta.scripts
|
|
244
|
-
gitnexus_condition: getGitNexusCondition(lang)
|
|
228
|
+
scripts: meta.scripts
|
|
245
229
|
};
|
|
246
230
|
}
|
|
247
231
|
|
|
@@ -251,7 +235,7 @@ function renderAgentsMd(meta, lang) {
|
|
|
251
235
|
const templatePath = path4.join(getTemplatesDir(), templateName);
|
|
252
236
|
const source = readFileSync4(templatePath, "utf-8");
|
|
253
237
|
const template = Handlebars.compile(source);
|
|
254
|
-
const data = buildAgentsTemplateData(meta
|
|
238
|
+
const data = buildAgentsTemplateData(meta);
|
|
255
239
|
return template(data);
|
|
256
240
|
}
|
|
257
241
|
function generateAgentsMd(meta, lang, projectRoot = process.cwd()) {
|
|
@@ -263,30 +247,6 @@ function generateAgentsMd(meta, lang, projectRoot = process.cwd()) {
|
|
|
263
247
|
const content = renderAgentsMd(meta, lang);
|
|
264
248
|
writeFileSync2(agentsPath, content, "utf-8");
|
|
265
249
|
}
|
|
266
|
-
function updateGraphTimestampInAgents(projectRoot = process.cwd(), timestamp) {
|
|
267
|
-
const agentsPath = path4.join(projectRoot, "AGENTS.md");
|
|
268
|
-
if (!existsSync4(agentsPath)) return;
|
|
269
|
-
const ts = timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
270
|
-
let content = readFileSync4(agentsPath, "utf-8");
|
|
271
|
-
const marker = "\u56FE\u8C31\u6700\u540E\u5237\u65B0\u65F6\u95F4";
|
|
272
|
-
const enMarker = "Graph last refreshed";
|
|
273
|
-
const isEnglish = content.includes("## AI Working Guide");
|
|
274
|
-
const line = isEnglish ? `- ${enMarker}: ${ts}` : `- ${marker}: ${ts}`;
|
|
275
|
-
const regex = /- (图谱最后刷新时间|Graph last refreshed): .+/;
|
|
276
|
-
if (regex.test(content)) {
|
|
277
|
-
content = content.replace(regex, line);
|
|
278
|
-
} else if (content.includes("## AI \u5DE5\u4F5C\u6307\u5357") || isEnglish) {
|
|
279
|
-
const sectionRegex = /(## AI (工作指南|Working Guide)\n)/;
|
|
280
|
-
content = content.replace(sectionRegex, `$1${line}
|
|
281
|
-
`);
|
|
282
|
-
} else {
|
|
283
|
-
content += `
|
|
284
|
-
## AI \u5DE5\u4F5C\u6307\u5357
|
|
285
|
-
${line}
|
|
286
|
-
`;
|
|
287
|
-
}
|
|
288
|
-
writeFileSync2(agentsPath, content, "utf-8");
|
|
289
|
-
}
|
|
290
250
|
|
|
291
251
|
// src/core/i18n.ts
|
|
292
252
|
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
@@ -315,68 +275,13 @@ function tf(key, lang, vars) {
|
|
|
315
275
|
}
|
|
316
276
|
|
|
317
277
|
// src/adapters/index.ts
|
|
318
|
-
import { homedir } from "os";
|
|
319
278
|
import path6 from "path";
|
|
320
|
-
import {
|
|
321
|
-
|
|
322
|
-
writeFileSync as writeFileSync3,
|
|
323
|
-
mkdirSync as mkdirSync2,
|
|
324
|
-
existsSync as existsSync6,
|
|
325
|
-
copyFileSync
|
|
326
|
-
} from "fs";
|
|
327
|
-
import deepmerge from "deepmerge";
|
|
328
|
-
var GITNEXUS_SNIPPET = {
|
|
329
|
-
gitnexus: {
|
|
330
|
-
command: "gitnexus",
|
|
331
|
-
args: ["mcp"],
|
|
332
|
-
disabled: false
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
function readJsonFile(filePath) {
|
|
336
|
-
if (!existsSync6(filePath)) {
|
|
337
|
-
return { mcpServers: {} };
|
|
338
|
-
}
|
|
339
|
-
const raw = readFileSync6(filePath, "utf-8");
|
|
340
|
-
try {
|
|
341
|
-
return JSON.parse(raw);
|
|
342
|
-
} catch {
|
|
343
|
-
return { mcpServers: {} };
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
function writeJsonFile(filePath, data) {
|
|
347
|
-
const dir = path6.dirname(filePath);
|
|
348
|
-
if (!existsSync6(dir)) {
|
|
349
|
-
mkdirSync2(dir, { recursive: true });
|
|
350
|
-
}
|
|
351
|
-
writeFileSync3(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
352
|
-
}
|
|
353
|
-
function ensureMcpServersShape(config) {
|
|
354
|
-
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
355
|
-
config.mcpServers = {};
|
|
356
|
-
}
|
|
357
|
-
return config;
|
|
358
|
-
}
|
|
359
|
-
function createAdapter(id, rulesTargetPath, rulesTemplateName, mcpConfigPath) {
|
|
279
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync6, copyFileSync } from "fs";
|
|
280
|
+
function createAdapter(id, rulesTargetPath, rulesTemplateName) {
|
|
360
281
|
return {
|
|
361
282
|
id,
|
|
362
283
|
rulesTargetPath,
|
|
363
284
|
rulesTemplateName,
|
|
364
|
-
mcpConfigPath,
|
|
365
|
-
getGitNexusMcpEntry() {
|
|
366
|
-
return { ...GITNEXUS_SNIPPET };
|
|
367
|
-
},
|
|
368
|
-
mergeGitNexusMcp(config) {
|
|
369
|
-
const base = ensureMcpServersShape({ ...config });
|
|
370
|
-
const servers = base.mcpServers;
|
|
371
|
-
if (servers.gitnexus) {
|
|
372
|
-
return base;
|
|
373
|
-
}
|
|
374
|
-
return deepmerge(base, { mcpServers: GITNEXUS_SNIPPET });
|
|
375
|
-
},
|
|
376
|
-
hasGitNexusMcp(config) {
|
|
377
|
-
const servers = config.mcpServers;
|
|
378
|
-
return Boolean(servers?.gitnexus);
|
|
379
|
-
},
|
|
380
285
|
injectRules(projectRoot) {
|
|
381
286
|
const target = path6.join(projectRoot, rulesTargetPath);
|
|
382
287
|
const source = path6.join(getRulesTemplatesDir(), rulesTemplateName);
|
|
@@ -385,41 +290,23 @@ function createAdapter(id, rulesTargetPath, rulesTemplateName, mcpConfigPath) {
|
|
|
385
290
|
mkdirSync2(dir, { recursive: true });
|
|
386
291
|
}
|
|
387
292
|
copyFileSync(source, target);
|
|
388
|
-
},
|
|
389
|
-
async configureMcp() {
|
|
390
|
-
const configPath = mcpConfigPath;
|
|
391
|
-
const snippet = JSON.stringify(GITNEXUS_SNIPPET, null, 2);
|
|
392
|
-
try {
|
|
393
|
-
const existing = readJsonFile(configPath);
|
|
394
|
-
if (this.hasGitNexusMcp(existing)) {
|
|
395
|
-
return { success: true, path: configPath, snippet };
|
|
396
|
-
}
|
|
397
|
-
const merged = this.mergeGitNexusMcp(existing);
|
|
398
|
-
writeJsonFile(configPath, merged);
|
|
399
|
-
return { success: true, path: configPath, snippet };
|
|
400
|
-
} catch {
|
|
401
|
-
return { success: false, path: configPath, snippet };
|
|
402
|
-
}
|
|
403
293
|
}
|
|
404
294
|
};
|
|
405
295
|
}
|
|
406
296
|
var cursorAdapter = createAdapter(
|
|
407
297
|
"cursor",
|
|
408
298
|
".cursor/rules/karpathy-guidelines.mdc",
|
|
409
|
-
"cursor.mdc"
|
|
410
|
-
path6.join(homedir(), ".cursor", "mcp.json")
|
|
299
|
+
"cursor.mdc"
|
|
411
300
|
);
|
|
412
301
|
var codexAdapter = createAdapter(
|
|
413
302
|
"codex",
|
|
414
303
|
".codex/rules/karpathy-guidelines.md",
|
|
415
|
-
"codex.md"
|
|
416
|
-
path6.join(homedir(), ".codex", "mcp.json")
|
|
304
|
+
"codex.md"
|
|
417
305
|
);
|
|
418
306
|
var opencodeAdapter = createAdapter(
|
|
419
307
|
"opencode",
|
|
420
308
|
".opencode/rules/karpathy-guidelines.md",
|
|
421
|
-
"opencode.md"
|
|
422
|
-
path6.join(homedir(), ".config", "opencode", "mcp.json")
|
|
309
|
+
"opencode.md"
|
|
423
310
|
);
|
|
424
311
|
var adapters = {
|
|
425
312
|
cursor: cursorAdapter,
|
|
@@ -430,38 +317,419 @@ function getAdaptersForPlatforms(platforms) {
|
|
|
430
317
|
return platforms.map((p) => adapters[p]);
|
|
431
318
|
}
|
|
432
319
|
|
|
433
|
-
// src/adapters/
|
|
434
|
-
import
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
320
|
+
// src/adapters/cursor-commands.ts
|
|
321
|
+
import {
|
|
322
|
+
copyFileSync as copyFileSync2,
|
|
323
|
+
existsSync as existsSync7,
|
|
324
|
+
mkdirSync as mkdirSync3,
|
|
325
|
+
readdirSync
|
|
326
|
+
} from "fs";
|
|
327
|
+
import path7 from "path";
|
|
328
|
+
function injectCursorSlashCommands(projectRoot = process.cwd()) {
|
|
329
|
+
const sourceDir = getCursorCommandsTemplatesDir();
|
|
330
|
+
const targetDir = path7.join(projectRoot, ".cursor", "commands");
|
|
331
|
+
if (!existsSync7(sourceDir)) {
|
|
332
|
+
throw new Error(`Cursor command templates not found: ${sourceDir}`);
|
|
333
|
+
}
|
|
334
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
335
|
+
const files = readdirSync(sourceDir).filter((f) => f.endsWith(".md"));
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
copyFileSync2(path7.join(sourceDir, file), path7.join(targetDir, file));
|
|
338
|
+
}
|
|
339
|
+
return files.length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/adapters/visual-skills.ts
|
|
343
|
+
import {
|
|
344
|
+
copyFileSync as copyFileSync3,
|
|
345
|
+
existsSync as existsSync8,
|
|
346
|
+
mkdirSync as mkdirSync4,
|
|
347
|
+
readdirSync as readdirSync2,
|
|
348
|
+
statSync
|
|
349
|
+
} from "fs";
|
|
350
|
+
import path8 from "path";
|
|
351
|
+
var SKILL_NAME = "visual-fidelity";
|
|
352
|
+
function copyDirRecursive(source, target) {
|
|
353
|
+
mkdirSync4(target, { recursive: true });
|
|
354
|
+
let count = 0;
|
|
355
|
+
for (const entry of readdirSync2(source)) {
|
|
356
|
+
const srcPath = path8.join(source, entry);
|
|
357
|
+
const destPath = path8.join(target, entry);
|
|
358
|
+
if (statSync(srcPath).isDirectory()) {
|
|
359
|
+
count += copyDirRecursive(srcPath, destPath);
|
|
441
360
|
} else {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
361
|
+
copyFileSync3(srcPath, destPath);
|
|
362
|
+
count += 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return count;
|
|
366
|
+
}
|
|
367
|
+
function injectVisualFidelitySkill(projectRoot = process.cwd()) {
|
|
368
|
+
const sourceDir = path8.join(getSkillsTemplatesDir(), SKILL_NAME);
|
|
369
|
+
const targetDir = path8.join(projectRoot, ".cursor", "skills", SKILL_NAME);
|
|
370
|
+
if (!existsSync8(sourceDir)) {
|
|
371
|
+
throw new Error(`Visual fidelity skill templates not found: ${sourceDir}`);
|
|
372
|
+
}
|
|
373
|
+
return copyDirRecursive(sourceDir, targetDir);
|
|
374
|
+
}
|
|
375
|
+
function getVisualFidelitySkillPath(projectRoot) {
|
|
376
|
+
return path8.join(projectRoot, ".cursor", "skills", SKILL_NAME, "SKILL.md");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/core/visual-spec.ts
|
|
380
|
+
import {
|
|
381
|
+
existsSync as existsSync9,
|
|
382
|
+
mkdirSync as mkdirSync5,
|
|
383
|
+
readFileSync as readFileSync6,
|
|
384
|
+
writeFileSync as writeFileSync3
|
|
385
|
+
} from "fs";
|
|
386
|
+
import path9 from "path";
|
|
387
|
+
function parseFigmaUrl(url) {
|
|
388
|
+
const normalized = url.trim();
|
|
389
|
+
const match = /figma\.com\/(?:file|design)\/([a-zA-Z0-9]+)/i.exec(normalized) ?? /figma\.com\/(?:file|design)\/([a-zA-Z0-9]+)/i.exec(normalized);
|
|
390
|
+
if (!match?.[1]) return null;
|
|
391
|
+
const fileKey = match[1];
|
|
392
|
+
const nodeParam = /[?&]node-id=([^&\s)]+)/i.exec(normalized);
|
|
393
|
+
const nodeId = nodeParam?.[1]?.replace(/-/g, ":");
|
|
394
|
+
return nodeId ? { fileKey, nodeId } : { fileKey };
|
|
395
|
+
}
|
|
396
|
+
function extractFigmaLinks(text) {
|
|
397
|
+
const links = [];
|
|
398
|
+
const seen = /* @__PURE__ */ new Set();
|
|
399
|
+
for (const line of text.split("\n")) {
|
|
400
|
+
const urlMatch = /(https?:\/\/[^\s)]+)/g;
|
|
401
|
+
let m;
|
|
402
|
+
while ((m = urlMatch.exec(line)) !== null) {
|
|
403
|
+
const url = m[1];
|
|
404
|
+
if (!url.includes("figma.com")) continue;
|
|
405
|
+
if (seen.has(url)) continue;
|
|
406
|
+
seen.add(url);
|
|
407
|
+
const parsed = parseFigmaUrl(url);
|
|
408
|
+
const label = line.replace(url, "").replace(/^[-*•\s]+/, "").trim();
|
|
409
|
+
links.push({
|
|
410
|
+
url,
|
|
411
|
+
fileKey: parsed?.fileKey,
|
|
412
|
+
nodeId: parsed?.nodeId,
|
|
413
|
+
label: label || void 0
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return links;
|
|
418
|
+
}
|
|
419
|
+
function extractVisualDesignSection(agentsContent, lang) {
|
|
420
|
+
const headings = lang === "en" ? ["Visual Design", "Design"] : ["\u89C6\u89C9\u7A3F", "\u8BBE\u8BA1\u7A3F"];
|
|
421
|
+
for (const heading of headings) {
|
|
422
|
+
const regex = new RegExp(`## ${heading}[\\s\\n]+([\\s\\S]*?)(?=\\n## |$)`, "m");
|
|
423
|
+
const match = agentsContent.match(regex);
|
|
424
|
+
const section = match?.[1]?.trim() ?? "";
|
|
425
|
+
if (section && !section.includes("[NEEDS LLM INPUT]")) {
|
|
426
|
+
return section;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return "";
|
|
430
|
+
}
|
|
431
|
+
function getFtDir(projectRoot) {
|
|
432
|
+
return path9.join(projectRoot, ".ft");
|
|
433
|
+
}
|
|
434
|
+
function getVisualSpecPath(projectRoot) {
|
|
435
|
+
return path9.join(getFtDir(projectRoot), "visual-spec.md");
|
|
436
|
+
}
|
|
437
|
+
function getVisualChecklistPath(projectRoot) {
|
|
438
|
+
return path9.join(getFtDir(projectRoot), "visual-checklist.json");
|
|
439
|
+
}
|
|
440
|
+
function loadVisualSpecTemplate() {
|
|
441
|
+
const templatePath = path9.join(getTemplatesDir(), "visual-spec.md");
|
|
442
|
+
return readFileSync6(templatePath, "utf-8");
|
|
443
|
+
}
|
|
444
|
+
function defaultChecklist(frame = "TBD") {
|
|
445
|
+
return {
|
|
446
|
+
version: 1,
|
|
447
|
+
frame,
|
|
448
|
+
items: []
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function readVisualChecklist(projectRoot) {
|
|
452
|
+
const checklistPath = getVisualChecklistPath(projectRoot);
|
|
453
|
+
if (!existsSync9(checklistPath)) {
|
|
454
|
+
return defaultChecklist();
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
return JSON.parse(readFileSync6(checklistPath, "utf-8"));
|
|
458
|
+
} catch {
|
|
459
|
+
return defaultChecklist();
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function writeVisualChecklist(projectRoot, checklist) {
|
|
463
|
+
const ftDir = getFtDir(projectRoot);
|
|
464
|
+
mkdirSync5(ftDir, { recursive: true });
|
|
465
|
+
writeFileSync3(
|
|
466
|
+
getVisualChecklistPath(projectRoot),
|
|
467
|
+
`${JSON.stringify(checklist, null, 2)}
|
|
468
|
+
`,
|
|
469
|
+
"utf-8"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
function tokenToChecklistItem(token) {
|
|
473
|
+
return {
|
|
474
|
+
id: token.id,
|
|
475
|
+
category: token.category,
|
|
476
|
+
description: token.description,
|
|
477
|
+
designValue: token.value,
|
|
478
|
+
cssVariable: token.cssVariable,
|
|
479
|
+
status: "pending"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function mergeTokensIntoChecklist(checklist, tokens, frame, nodeId) {
|
|
483
|
+
const existingById = new Map(checklist.items.map((item) => [item.id, item]));
|
|
484
|
+
const mergedItems = tokens.map((token) => {
|
|
485
|
+
const existing = existingById.get(token.id);
|
|
486
|
+
if (existing?.status === "passed") {
|
|
487
|
+
return existing;
|
|
488
|
+
}
|
|
489
|
+
return tokenToChecklistItem(token);
|
|
490
|
+
});
|
|
491
|
+
for (const item of checklist.items) {
|
|
492
|
+
if (!mergedItems.some((m) => m.id === item.id)) {
|
|
493
|
+
mergedItems.push(item);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
...checklist,
|
|
498
|
+
frame: frame ?? checklist.frame,
|
|
499
|
+
figmaNodeId: nodeId ?? checklist.figmaNodeId,
|
|
500
|
+
items: mergedItems
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function renderTokenRows(tokens) {
|
|
504
|
+
if (tokens.length === 0) {
|
|
505
|
+
return "| TBD | TBD | TBD | TBD |";
|
|
506
|
+
}
|
|
507
|
+
return tokens.map(
|
|
508
|
+
(t2) => `| ${t2.id} | ${t2.value} | ${t2.cssVariable ?? "\u2014"} | ${t2.source ?? "figma"} |`
|
|
509
|
+
).join("\n");
|
|
510
|
+
}
|
|
511
|
+
function renderDesignSources(links, lang) {
|
|
512
|
+
if (links.length === 0) {
|
|
513
|
+
return lang === "en" ? "- TBD \u2014 add Figma/Lanhu links to AGENTS.md Visual Design section" : "- TBD \u2014 \u8BF7\u5728 AGENTS.md\u300C\u89C6\u89C9\u7A3F\u300D\u7AE0\u8282\u8865\u5145 Figma / \u84DD\u6E56\u94FE\u63A5";
|
|
514
|
+
}
|
|
515
|
+
return links.map((link) => {
|
|
516
|
+
const parts = [link.url];
|
|
517
|
+
if (link.fileKey) parts.push(`fileKey: \`${link.fileKey}\``);
|
|
518
|
+
if (link.nodeId) parts.push(`nodeId: \`${link.nodeId}\``);
|
|
519
|
+
if (link.label) parts.unshift(`**${link.label}** \u2014`);
|
|
520
|
+
return `- ${parts.join(" \xB7 ")}`;
|
|
521
|
+
}).join("\n");
|
|
522
|
+
}
|
|
523
|
+
function writeVisualSpec(projectRoot, links, tokens, lang) {
|
|
524
|
+
const ftDir = getFtDir(projectRoot);
|
|
525
|
+
mkdirSync5(ftDir, { recursive: true });
|
|
526
|
+
const template = loadVisualSpecTemplate();
|
|
527
|
+
const content = template.replace("{{designSources}}", renderDesignSources(links, lang)).replace("{{tokenRows}}", renderTokenRows(tokens));
|
|
528
|
+
writeFileSync3(getVisualSpecPath(projectRoot), content, "utf-8");
|
|
529
|
+
}
|
|
530
|
+
function ensureVisualSpec(projectRoot, lang, designSection, tokens = [], primaryLink) {
|
|
531
|
+
const links = extractFigmaLinks(designSection);
|
|
532
|
+
const specExists = existsSync9(getVisualSpecPath(projectRoot));
|
|
533
|
+
const checklist = readVisualChecklist(projectRoot);
|
|
534
|
+
const frameName = primaryLink?.label ?? primaryLink?.nodeId ?? checklist.frame ?? "TBD";
|
|
535
|
+
const mergedChecklist = mergeTokensIntoChecklist(
|
|
536
|
+
checklist,
|
|
537
|
+
tokens,
|
|
538
|
+
frameName,
|
|
539
|
+
primaryLink?.nodeId
|
|
540
|
+
);
|
|
541
|
+
writeVisualSpec(projectRoot, links, tokens, lang);
|
|
542
|
+
writeVisualChecklist(projectRoot, mergedChecklist);
|
|
543
|
+
return {
|
|
544
|
+
specPath: getVisualSpecPath(projectRoot),
|
|
545
|
+
checklistPath: getVisualChecklistPath(projectRoot),
|
|
546
|
+
created: !specExists,
|
|
547
|
+
tokenCount: tokens.length,
|
|
548
|
+
linkCount: links.length
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function countChecklistByStatus(checklist) {
|
|
552
|
+
const passed = checklist.items.filter((i) => i.status === "passed").length;
|
|
553
|
+
const failed = checklist.items.filter((i) => i.status === "failed").length;
|
|
554
|
+
const pending = checklist.items.filter((i) => i.status === "pending").length;
|
|
555
|
+
return { passed, pending, failed, total: checklist.items.length };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/core/visual-verify.ts
|
|
559
|
+
import {
|
|
560
|
+
existsSync as existsSync10,
|
|
561
|
+
mkdirSync as mkdirSync6,
|
|
562
|
+
readFileSync as readFileSync7,
|
|
563
|
+
writeFileSync as writeFileSync4
|
|
564
|
+
} from "fs";
|
|
565
|
+
import path10 from "path";
|
|
566
|
+
import { execa as execa2 } from "execa";
|
|
567
|
+
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
568
|
+
function getVisualVerifyConfigPath(projectRoot) {
|
|
569
|
+
return path10.join(getFtDir(projectRoot), "visual-verify.json");
|
|
570
|
+
}
|
|
571
|
+
function getVisualBaselinesDir(projectRoot) {
|
|
572
|
+
return path10.join(getFtDir(projectRoot), "visual-baselines");
|
|
573
|
+
}
|
|
574
|
+
function readVisualVerifyConfig(projectRoot) {
|
|
575
|
+
const configPath = getVisualVerifyConfigPath(projectRoot);
|
|
576
|
+
if (!existsSync10(configPath)) return null;
|
|
577
|
+
try {
|
|
578
|
+
return JSON.parse(readFileSync7(configPath, "utf-8"));
|
|
579
|
+
} catch {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function ensureVisualVerifyConfig(projectRoot) {
|
|
584
|
+
const ftDir = getFtDir(projectRoot);
|
|
585
|
+
mkdirSync6(ftDir, { recursive: true });
|
|
586
|
+
mkdirSync6(getVisualBaselinesDir(projectRoot), { recursive: true });
|
|
587
|
+
const configPath = getVisualVerifyConfigPath(projectRoot);
|
|
588
|
+
if (!existsSync10(configPath)) {
|
|
589
|
+
const templatePath = path10.join(getTemplatesDir(), "visual-verify.json");
|
|
590
|
+
writeFileSync4(configPath, readFileSync7(templatePath, "utf-8"), "utf-8");
|
|
591
|
+
}
|
|
592
|
+
return configPath;
|
|
593
|
+
}
|
|
594
|
+
function resolveProjectDependency(projectRoot, packageName) {
|
|
595
|
+
const pkgPath = path10.join(projectRoot, "node_modules", packageName, "package.json");
|
|
596
|
+
return existsSync10(pkgPath) ? path10.join(projectRoot, "node_modules", packageName) : null;
|
|
597
|
+
}
|
|
598
|
+
function hasVisualDiffDependencies(projectRoot) {
|
|
599
|
+
return resolveProjectDependency(projectRoot, "playwright") !== null && resolveProjectDependency(projectRoot, "pixelmatch") !== null && resolveProjectDependency(projectRoot, "pngjs") !== null;
|
|
600
|
+
}
|
|
601
|
+
async function runVisualDiff(projectRoot) {
|
|
602
|
+
const scriptPath = path10.join(getScriptsDir(), "visual-diff.mjs");
|
|
603
|
+
if (!existsSync10(scriptPath)) {
|
|
604
|
+
return { exitCode: 1, report: null, stdout: "" };
|
|
605
|
+
}
|
|
606
|
+
const result = await execa2("node", [scriptPath], {
|
|
607
|
+
cwd: projectRoot,
|
|
608
|
+
reject: false
|
|
464
609
|
});
|
|
610
|
+
let report = null;
|
|
611
|
+
const jsonLine = result.stdout.split("\n").map((line) => line.trim()).find((line) => line.startsWith("{") && line.includes('"results"'));
|
|
612
|
+
if (jsonLine) {
|
|
613
|
+
try {
|
|
614
|
+
report = JSON.parse(jsonLine);
|
|
615
|
+
} catch {
|
|
616
|
+
report = null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
exitCode: result.exitCode ?? 1,
|
|
621
|
+
report,
|
|
622
|
+
stdout: [result.stdout, result.stderr].filter(Boolean).join("\n")
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function formatVerifyConfigSummary(config, lang) {
|
|
626
|
+
const lines = config.screens.map(
|
|
627
|
+
(s) => ` - ${s.id}: ${config.baseUrl}${s.path} \u2194 ${s.baseline} (${s.viewport?.width ?? DEFAULT_VIEWPORT.width}\xD7${s.viewport?.height ?? DEFAULT_VIEWPORT.height})`
|
|
628
|
+
);
|
|
629
|
+
return lines.join("\n") || (lang === "en" ? " (no screens configured)" : " \uFF08\u672A\u914D\u7F6E\u9875\u9762\uFF09");
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/adapters/figma-mcp.ts
|
|
633
|
+
import {
|
|
634
|
+
existsSync as existsSync11,
|
|
635
|
+
mkdirSync as mkdirSync7,
|
|
636
|
+
readFileSync as readFileSync8,
|
|
637
|
+
writeFileSync as writeFileSync5
|
|
638
|
+
} from "fs";
|
|
639
|
+
import path11 from "path";
|
|
640
|
+
import chalk from "chalk";
|
|
641
|
+
import inquirer from "inquirer";
|
|
642
|
+
var FIGMA_SERVER_ID = "figma-developer-mcp";
|
|
643
|
+
var FIGMA_DOCS_URL = "https://www.figma.com/developers/api#access-tokens";
|
|
644
|
+
function getMcpPath(projectRoot) {
|
|
645
|
+
return path11.join(projectRoot, ".cursor", "mcp.json");
|
|
646
|
+
}
|
|
647
|
+
function readMcpJson(projectRoot) {
|
|
648
|
+
const mcpPath = getMcpPath(projectRoot);
|
|
649
|
+
if (!existsSync11(mcpPath)) {
|
|
650
|
+
return { mcpServers: {} };
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return JSON.parse(readFileSync8(mcpPath, "utf-8"));
|
|
654
|
+
} catch {
|
|
655
|
+
return { mcpServers: {} };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function writeMcpJson(projectRoot, config) {
|
|
659
|
+
const cursorDir = path11.join(projectRoot, ".cursor");
|
|
660
|
+
mkdirSync7(cursorDir, { recursive: true });
|
|
661
|
+
writeFileSync5(getMcpPath(projectRoot), `${JSON.stringify(config, null, 2)}
|
|
662
|
+
`, "utf-8");
|
|
663
|
+
}
|
|
664
|
+
function buildFigmaMcpServerEntry(apiKey) {
|
|
665
|
+
const args = ["-y", "figma-developer-mcp", "--stdio"];
|
|
666
|
+
const env = { FIGMA_API_KEY: apiKey };
|
|
667
|
+
if (process.platform === "win32") {
|
|
668
|
+
return {
|
|
669
|
+
command: "cmd",
|
|
670
|
+
args: ["/c", "npx", ...args],
|
|
671
|
+
env
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
command: "npx",
|
|
676
|
+
args,
|
|
677
|
+
env
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function mergeFigmaMcpConfig(projectRoot, apiKey) {
|
|
681
|
+
const existing = readMcpJson(projectRoot);
|
|
682
|
+
const servers = existing.mcpServers ?? existing.servers ?? {};
|
|
683
|
+
const entry = buildFigmaMcpServerEntry(apiKey);
|
|
684
|
+
const next = {
|
|
685
|
+
...existing,
|
|
686
|
+
mcpServers: {
|
|
687
|
+
...servers,
|
|
688
|
+
[FIGMA_SERVER_ID]: entry
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
delete next.servers;
|
|
692
|
+
writeMcpJson(projectRoot, next);
|
|
693
|
+
}
|
|
694
|
+
function isFigmaMcpConfigured(projectRoot = process.cwd()) {
|
|
695
|
+
const mcpPath = getMcpPath(projectRoot);
|
|
696
|
+
if (!existsSync11(mcpPath)) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
const content = readFileSync8(mcpPath, "utf-8");
|
|
700
|
+
return content.includes(FIGMA_SERVER_ID) || content.includes("figma-developer-mcp") || content.includes("FIGMA_API_KEY");
|
|
701
|
+
}
|
|
702
|
+
async function setupFigmaMcp(projectRoot, lang) {
|
|
703
|
+
console.log("");
|
|
704
|
+
console.log(t("init.figmaIntro", lang));
|
|
705
|
+
console.log(t("init.figmaDocsHint", lang));
|
|
706
|
+
console.log(` ${FIGMA_DOCS_URL}`);
|
|
707
|
+
console.log("");
|
|
708
|
+
const { configure } = await inquirer.prompt([
|
|
709
|
+
{
|
|
710
|
+
type: "confirm",
|
|
711
|
+
name: "configure",
|
|
712
|
+
message: t("init.figmaConfigureQuestion", lang),
|
|
713
|
+
default: true
|
|
714
|
+
}
|
|
715
|
+
]);
|
|
716
|
+
if (!configure) {
|
|
717
|
+
console.log(chalk.yellow(t("init.figmaSkipped", lang)));
|
|
718
|
+
return { enabled: false, configured: false };
|
|
719
|
+
}
|
|
720
|
+
const { apiKey } = await inquirer.prompt([
|
|
721
|
+
{
|
|
722
|
+
type: "password",
|
|
723
|
+
name: "apiKey",
|
|
724
|
+
message: t("init.figmaApiKeyQuestion", lang),
|
|
725
|
+
mask: "*",
|
|
726
|
+
validate: (value) => value.trim().length > 0 ? true : t("init.figmaApiKeyRequired", lang)
|
|
727
|
+
}
|
|
728
|
+
]);
|
|
729
|
+
mergeFigmaMcpConfig(projectRoot, apiKey.trim());
|
|
730
|
+
console.log(chalk.green(tf("init.figmaConfigured", lang, { path: ".cursor/mcp.json" })));
|
|
731
|
+
console.log(chalk.yellow(t("init.figmaGitignoreHint", lang)));
|
|
732
|
+
return { enabled: true, configured: true };
|
|
465
733
|
}
|
|
466
734
|
|
|
467
735
|
// src/cli/commands/init.ts
|
|
@@ -474,7 +742,7 @@ async function selectPlatforms(mode, lang) {
|
|
|
474
742
|
if (detected.length > 0) return detected;
|
|
475
743
|
return ["cursor", "codex", "opencode"];
|
|
476
744
|
}
|
|
477
|
-
const { platforms } = await
|
|
745
|
+
const { platforms } = await inquirer2.prompt([
|
|
478
746
|
{
|
|
479
747
|
type: "checkbox",
|
|
480
748
|
name: "platforms",
|
|
@@ -490,7 +758,7 @@ async function selectPlatforms(mode, lang) {
|
|
|
490
758
|
return platforms;
|
|
491
759
|
}
|
|
492
760
|
async function selectInjectMode(lang) {
|
|
493
|
-
const { mode } = await
|
|
761
|
+
const { mode } = await inquirer2.prompt([
|
|
494
762
|
{
|
|
495
763
|
type: "list",
|
|
496
764
|
name: "mode",
|
|
@@ -517,11 +785,6 @@ async function runInit(options) {
|
|
|
517
785
|
console.error(chalk2.red(t("error.karpathyMissing", language)));
|
|
518
786
|
process.exit(1);
|
|
519
787
|
}
|
|
520
|
-
const gitnexusVersion = await detectGitNexus();
|
|
521
|
-
const gitnexusInstalled = Boolean(gitnexusVersion);
|
|
522
|
-
if (!gitnexusInstalled) {
|
|
523
|
-
console.log(chalk2.gray(t("hint.gitnexusOptional", language)));
|
|
524
|
-
}
|
|
525
788
|
const injectMode = await selectInjectMode(language);
|
|
526
789
|
const selectedPlatforms = await selectPlatforms(injectMode, language);
|
|
527
790
|
const adapterList = getAdaptersForPlatforms(selectedPlatforms);
|
|
@@ -529,8 +792,26 @@ async function runInit(options) {
|
|
|
529
792
|
adapter.injectRules(projectRoot);
|
|
530
793
|
console.log(chalk2.green(tf("init.rulesInjected", language, { path: adapter.rulesTargetPath })));
|
|
531
794
|
}
|
|
532
|
-
if (
|
|
533
|
-
|
|
795
|
+
if (selectedPlatforms.includes("cursor")) {
|
|
796
|
+
const commandCount = injectCursorSlashCommands(projectRoot);
|
|
797
|
+
console.log(
|
|
798
|
+
chalk2.green(tf("init.cursorCommandsInjected", language, { count: String(commandCount) }))
|
|
799
|
+
);
|
|
800
|
+
const skillFileCount = injectVisualFidelitySkill(projectRoot);
|
|
801
|
+
console.log(
|
|
802
|
+
chalk2.green(tf("init.visualSkillInjected", language, { count: String(skillFileCount) }))
|
|
803
|
+
);
|
|
804
|
+
ensureVisualSpec(projectRoot, language, "");
|
|
805
|
+
ensureVisualVerifyConfig(projectRoot);
|
|
806
|
+
console.log(chalk2.green(t("init.visualSpecScaffolded", language)));
|
|
807
|
+
}
|
|
808
|
+
let figmaConfig = existingConfig.figma ?? { enabled: false, configured: false };
|
|
809
|
+
if (selectedPlatforms.includes("cursor")) {
|
|
810
|
+
if (isFigmaMcpConfigured(projectRoot)) {
|
|
811
|
+
figmaConfig = { enabled: true, configured: true };
|
|
812
|
+
} else {
|
|
813
|
+
figmaConfig = await setupFigmaMcp(projectRoot, language);
|
|
814
|
+
}
|
|
534
815
|
}
|
|
535
816
|
const meta = detectProjectMeta(projectRoot);
|
|
536
817
|
generateAgentsMd(meta, language, projectRoot);
|
|
@@ -538,28 +819,29 @@ async function runInit(options) {
|
|
|
538
819
|
const config = mergeConfig({
|
|
539
820
|
...existingConfig,
|
|
540
821
|
language,
|
|
822
|
+
figma: figmaConfig,
|
|
541
823
|
tools: selectedPlatforms
|
|
542
824
|
});
|
|
543
825
|
writeDefaultConfig(projectRoot, config);
|
|
544
826
|
console.log(chalk2.blue(t("init.runningComet", language)));
|
|
545
|
-
await
|
|
827
|
+
await execa3("comet", ["init"], { stdio: "inherit", cwd: projectRoot });
|
|
546
828
|
console.log(chalk2.green.bold(t("init.success", language)));
|
|
547
829
|
console.log(chalk2.cyan(t("init.nextStep", language)));
|
|
548
830
|
}
|
|
549
831
|
|
|
550
832
|
// src/cli/commands/update.ts
|
|
551
|
-
import { execa as
|
|
833
|
+
import { execa as execa4 } from "execa";
|
|
552
834
|
async function runUpdate() {
|
|
553
|
-
await
|
|
835
|
+
await execa4("npm", ["update", "-g", "@nick848/ft"], { stdio: "inherit" });
|
|
554
836
|
}
|
|
555
837
|
|
|
556
838
|
// src/cli/commands/version.ts
|
|
557
|
-
import { readFileSync as
|
|
558
|
-
import
|
|
839
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
840
|
+
import path12 from "path";
|
|
559
841
|
function getFtVersion() {
|
|
560
842
|
try {
|
|
561
|
-
const pkgPath =
|
|
562
|
-
const pkg = JSON.parse(
|
|
843
|
+
const pkgPath = path12.join(getPackageRoot(), "package.json");
|
|
844
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
563
845
|
return pkg.version ?? "unknown";
|
|
564
846
|
} catch {
|
|
565
847
|
return "unknown";
|
|
@@ -568,47 +850,42 @@ function getFtVersion() {
|
|
|
568
850
|
async function runVersion() {
|
|
569
851
|
const ftVersion = getFtVersion();
|
|
570
852
|
const cometVersion = await detectComet();
|
|
571
|
-
|
|
572
|
-
console.log(`
|
|
573
|
-
console.log(`Comet: ${cometVersion ?? "not installed"}`);
|
|
574
|
-
console.log(`GitNexus: ${gitnexusVersion ?? "not installed"}`);
|
|
853
|
+
console.log(`FT: ${ftVersion}`);
|
|
854
|
+
console.log(`Comet: ${cometVersion ?? "not installed"}`);
|
|
575
855
|
}
|
|
576
856
|
|
|
577
857
|
// src/cli/commands/help.ts
|
|
578
858
|
function printHelp() {
|
|
579
859
|
console.log(`
|
|
580
|
-
FT (Frontend Toolkit) \u2014 CLI orchestration for Comet
|
|
860
|
+
FT (Frontend Toolkit) \u2014 CLI orchestration for Comet and Karpathy rules
|
|
581
861
|
|
|
582
862
|
Usage:
|
|
583
863
|
ft init [--lang en|zh-CN] Full project initialization pipeline
|
|
584
864
|
ft update Update @nick848/ft globally
|
|
585
|
-
ft version Show FT
|
|
865
|
+
ft version Show FT and Comet versions
|
|
586
866
|
ft help Show this help
|
|
587
867
|
ft slash <command> [args] Run IDE slash command (internal)
|
|
588
868
|
|
|
589
869
|
Slash commands (use via IDE, maps to ft slash):
|
|
590
|
-
fill-context
|
|
591
|
-
open
|
|
592
|
-
design
|
|
593
|
-
build
|
|
594
|
-
verify
|
|
595
|
-
archive
|
|
596
|
-
hotfix
|
|
597
|
-
tweak
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
graph-refresh gitnexus analyze --force
|
|
601
|
-
graph-handoff Graph summary + prompt + AGENTS timestamp
|
|
602
|
-
graph-status Check GitNexus installation and graph state
|
|
870
|
+
fill-context Output prompt recipe to fill AGENTS.md
|
|
871
|
+
open Comet open (passthrough)
|
|
872
|
+
design Comet design (passthrough)
|
|
873
|
+
build Comet build (passthrough)
|
|
874
|
+
verify Comet verify (passthrough)
|
|
875
|
+
archive Comet archive (passthrough)
|
|
876
|
+
hotfix Comet hotfix (passthrough)
|
|
877
|
+
tweak Comet tweak (passthrough)
|
|
878
|
+
sync-cursor-commands Reinstall Cursor slash commands
|
|
879
|
+
visual-tools-install Install playwright, pixelmatch, pngjs for pixel diff
|
|
603
880
|
`);
|
|
604
881
|
}
|
|
605
882
|
|
|
606
883
|
// src/cli/commands/slash.ts
|
|
607
|
-
import
|
|
884
|
+
import chalk8 from "chalk";
|
|
608
885
|
|
|
609
886
|
// src/slash/fill-context.ts
|
|
610
|
-
import { readFileSync as
|
|
611
|
-
import
|
|
887
|
+
import { readFileSync as readFileSync10, existsSync as existsSync12 } from "fs";
|
|
888
|
+
import path13 from "path";
|
|
612
889
|
var PLACEHOLDER = "[NEEDS LLM INPUT]";
|
|
613
890
|
var SECTION_HINTS = {
|
|
614
891
|
"\u7ED3\u6784": { zh: "\u8BF7\u904D\u5386 src/ \u751F\u6210\u6811\u5F62\u76EE\u5F55\u7ED3\u6784", en: "Traverse src/ and produce a tree directory structure" },
|
|
@@ -616,7 +893,19 @@ var SECTION_HINTS = {
|
|
|
616
893
|
"\u89C4\u8303": { zh: "\u8BF7\u6839\u636E .eslintrc / prettier \u7B49\u914D\u7F6E\u6587\u4EF6\u603B\u7ED3\u4EE3\u7801\u89C4\u8303", en: "Summarize code conventions from eslint/prettier configs" },
|
|
617
894
|
"Conventions": { zh: "\u8BF7\u6839\u636E .eslintrc / prettier \u7B49\u914D\u7F6E\u6587\u4EF6\u603B\u7ED3\u4EE3\u7801\u89C4\u8303", en: "Summarize code conventions from eslint/prettier configs" },
|
|
618
895
|
"\u8DEF\u7531": { zh: "\u8BF7\u6839\u636E\u8DEF\u7531\u914D\u7F6E\u6587\u4EF6\u5217\u51FA\u4E3B\u8981\u8DEF\u7531", en: "List main routes from routing config" },
|
|
619
|
-
"Routing": { zh: "\u8BF7\u6839\u636E\u8DEF\u7531\u914D\u7F6E\u6587\u4EF6\u5217\u51FA\u4E3B\u8981\u8DEF\u7531", en: "List main routes from routing config" }
|
|
896
|
+
"Routing": { zh: "\u8BF7\u6839\u636E\u8DEF\u7531\u914D\u7F6E\u6587\u4EF6\u5217\u51FA\u4E3B\u8981\u8DEF\u7531", en: "List main routes from routing config" },
|
|
897
|
+
"\u89C6\u89C9\u7A3F": {
|
|
898
|
+
zh: "\u8BF7\u586B\u5199 Figma / \u84DD\u6E56\u7B49\u89C6\u89C9\u7A3F\u94FE\u63A5\uFF0C\u542B frame \u6216 node-id\uFF1B\u5982\u6709\u8BBE\u8BA1\u89C4\u8303\u4E00\u5E76\u8BF4\u660E",
|
|
899
|
+
en: "Add Figma / Lanhu design links with frame or node-id; include design tokens if any"
|
|
900
|
+
},
|
|
901
|
+
"Visual Design": {
|
|
902
|
+
zh: "\u8BF7\u586B\u5199 Figma / \u84DD\u6E56\u7B49\u89C6\u89C9\u7A3F\u94FE\u63A5\uFF0C\u542B frame \u6216 node-id\uFF1B\u5982\u6709\u8BBE\u8BA1\u89C4\u8303\u4E00\u5E76\u8BF4\u660E",
|
|
903
|
+
en: "Add Figma / Lanhu design links with frame or node-id; include design tokens if any"
|
|
904
|
+
},
|
|
905
|
+
"Design": {
|
|
906
|
+
zh: "\u8BF7\u586B\u5199 Figma / \u84DD\u6E56\u7B49\u89C6\u89C9\u7A3F\u94FE\u63A5\uFF0C\u542B frame \u6216 node-id\uFF1B\u5982\u6709\u8BBE\u8BA1\u89C4\u8303\u4E00\u5E76\u8BF4\u660E",
|
|
907
|
+
en: "Add Figma / Lanhu design links with frame or node-id; include design tokens if any"
|
|
908
|
+
}
|
|
620
909
|
};
|
|
621
910
|
function extractSections(content) {
|
|
622
911
|
const lines = content.split("\n");
|
|
@@ -641,16 +930,16 @@ function findSectionInBackup(section, backup) {
|
|
|
641
930
|
}
|
|
642
931
|
async function runFillContext(config) {
|
|
643
932
|
const projectRoot = process.cwd();
|
|
644
|
-
const agentsPath =
|
|
933
|
+
const agentsPath = path13.join(projectRoot, "AGENTS.md");
|
|
645
934
|
const lang = config.language;
|
|
646
|
-
if (!
|
|
935
|
+
if (!existsSync12(agentsPath)) {
|
|
647
936
|
console.log(t("fillContext.noAgents", lang));
|
|
648
937
|
return;
|
|
649
938
|
}
|
|
650
|
-
const agentsContent =
|
|
939
|
+
const agentsContent = readFileSync10(agentsPath, "utf-8");
|
|
651
940
|
const placeholders = extractSections(agentsContent);
|
|
652
|
-
const backupPath =
|
|
653
|
-
const backupContent =
|
|
941
|
+
const backupPath = path13.join(projectRoot, "AGENTS-BAK.md");
|
|
942
|
+
const backupContent = existsSync12(backupPath) ? readFileSync10(backupPath, "utf-8") : "";
|
|
654
943
|
const langLabel = lang === "en" ? "English" : "\u7B80\u4F53\u4E2D\u6587";
|
|
655
944
|
console.log(`# ${t("fillContext.title", lang)}
|
|
656
945
|
`);
|
|
@@ -687,8 +976,464 @@ async function runFillContext(config) {
|
|
|
687
976
|
}
|
|
688
977
|
}
|
|
689
978
|
|
|
979
|
+
// src/slash/build.ts
|
|
980
|
+
import { readFileSync as readFileSync12, existsSync as existsSync14 } from "fs";
|
|
981
|
+
import path15 from "path";
|
|
982
|
+
import chalk3 from "chalk";
|
|
983
|
+
import inquirer3 from "inquirer";
|
|
984
|
+
|
|
985
|
+
// src/adapters/figma-api.ts
|
|
986
|
+
import { readFileSync as readFileSync11, existsSync as existsSync13 } from "fs";
|
|
987
|
+
import path14 from "path";
|
|
988
|
+
var FIGMA_SERVER_ID2 = "figma-developer-mcp";
|
|
989
|
+
function readFigmaApiKeyFromMcp(projectRoot) {
|
|
990
|
+
const mcpPath = path14.join(projectRoot, ".cursor", "mcp.json");
|
|
991
|
+
if (!existsSync13(mcpPath)) return null;
|
|
992
|
+
try {
|
|
993
|
+
const raw = JSON.parse(readFileSync11(mcpPath, "utf-8"));
|
|
994
|
+
const servers = raw.mcpServers ?? raw.servers ?? {};
|
|
995
|
+
const figma = servers[FIGMA_SERVER_ID2];
|
|
996
|
+
const key = figma?.env?.FIGMA_API_KEY;
|
|
997
|
+
return key?.trim() || null;
|
|
998
|
+
} catch {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
function rgbaToHex(color) {
|
|
1003
|
+
const toByte = (v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0");
|
|
1004
|
+
const alpha = color.a ?? 1;
|
|
1005
|
+
if (alpha < 1) {
|
|
1006
|
+
return `rgba(${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}, ${alpha.toFixed(2)})`;
|
|
1007
|
+
}
|
|
1008
|
+
return `#${toByte(color.r)}${toByte(color.g)}${toByte(color.b)}`.toUpperCase();
|
|
1009
|
+
}
|
|
1010
|
+
function slugify(name) {
|
|
1011
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
|
1012
|
+
}
|
|
1013
|
+
function extractColorTokens(node, tokens, seen) {
|
|
1014
|
+
if (!node.fills?.length) return;
|
|
1015
|
+
for (const fill of node.fills) {
|
|
1016
|
+
if (fill.type !== "SOLID" || !fill.color) continue;
|
|
1017
|
+
const hex = rgbaToHex(fill.color);
|
|
1018
|
+
const base = slugify(node.name ?? "color") || "fill";
|
|
1019
|
+
const id = `color-${base}`;
|
|
1020
|
+
if (seen.has(id)) continue;
|
|
1021
|
+
seen.add(id);
|
|
1022
|
+
tokens.push({
|
|
1023
|
+
id,
|
|
1024
|
+
category: "color",
|
|
1025
|
+
description: `Fill color: ${node.name ?? "unnamed"}`,
|
|
1026
|
+
value: hex,
|
|
1027
|
+
cssVariable: `--color-${base}`,
|
|
1028
|
+
source: "figma-api"
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function extractTypographyTokens(node, tokens, seen) {
|
|
1033
|
+
if (node.type !== "TEXT" || !node.style) return;
|
|
1034
|
+
const { fontFamily, fontSize, fontWeight, lineHeightPx } = node.style;
|
|
1035
|
+
if (!fontSize) return;
|
|
1036
|
+
const base = slugify(node.name ?? "text") || "text";
|
|
1037
|
+
const id = `typography-${base}`;
|
|
1038
|
+
if (seen.has(id)) return;
|
|
1039
|
+
seen.add(id);
|
|
1040
|
+
const parts = [
|
|
1041
|
+
fontSize ? `${fontSize}px` : null,
|
|
1042
|
+
fontWeight ? `weight ${fontWeight}` : null,
|
|
1043
|
+
fontFamily ?? null,
|
|
1044
|
+
lineHeightPx ? `line-height ${lineHeightPx}px` : null
|
|
1045
|
+
].filter(Boolean);
|
|
1046
|
+
tokens.push({
|
|
1047
|
+
id,
|
|
1048
|
+
category: "typography",
|
|
1049
|
+
description: `Text: ${node.name ?? node.characters?.slice(0, 24) ?? "unnamed"}`,
|
|
1050
|
+
value: parts.join(" / "),
|
|
1051
|
+
cssVariable: `--text-${base}`,
|
|
1052
|
+
source: "figma-api"
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
function extractSpacingTokens(node, tokens, seen) {
|
|
1056
|
+
if (!node.layoutMode || node.layoutMode === "NONE") return;
|
|
1057
|
+
const base = slugify(node.name ?? "frame") || "frame";
|
|
1058
|
+
const paddings = [
|
|
1059
|
+
["padding-top", node.paddingTop],
|
|
1060
|
+
["padding-right", node.paddingRight],
|
|
1061
|
+
["padding-bottom", node.paddingBottom],
|
|
1062
|
+
["padding-left", node.paddingLeft]
|
|
1063
|
+
];
|
|
1064
|
+
for (const [prop, value] of paddings) {
|
|
1065
|
+
if (value === void 0 || value === 0) continue;
|
|
1066
|
+
const id = `spacing-${base}-${prop}`;
|
|
1067
|
+
if (seen.has(id)) continue;
|
|
1068
|
+
seen.add(id);
|
|
1069
|
+
tokens.push({
|
|
1070
|
+
id,
|
|
1071
|
+
category: "spacing",
|
|
1072
|
+
description: `${node.name ?? "Frame"} ${prop}`,
|
|
1073
|
+
value: `${value}px`,
|
|
1074
|
+
cssVariable: `--${base}-${prop}`,
|
|
1075
|
+
source: "figma-api"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
|
|
1079
|
+
const id = `spacing-${base}-gap`;
|
|
1080
|
+
if (!seen.has(id)) {
|
|
1081
|
+
seen.add(id);
|
|
1082
|
+
tokens.push({
|
|
1083
|
+
id,
|
|
1084
|
+
category: "spacing",
|
|
1085
|
+
description: `${node.name ?? "Frame"} item gap`,
|
|
1086
|
+
value: `${node.itemSpacing}px`,
|
|
1087
|
+
cssVariable: `--${base}-gap`,
|
|
1088
|
+
source: "figma-api"
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function walkNode(node, tokens, seen) {
|
|
1094
|
+
extractColorTokens(node, tokens, seen);
|
|
1095
|
+
extractTypographyTokens(node, tokens, seen);
|
|
1096
|
+
extractSpacingTokens(node, tokens, seen);
|
|
1097
|
+
for (const child of node.children ?? []) {
|
|
1098
|
+
walkNode(child, tokens, seen);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async function fetchFigmaDesignTokens(projectRoot, fileKey, nodeId) {
|
|
1102
|
+
const apiKey = readFigmaApiKeyFromMcp(projectRoot);
|
|
1103
|
+
if (!apiKey) return [];
|
|
1104
|
+
const ids = nodeId ? encodeURIComponent(nodeId) : void 0;
|
|
1105
|
+
const url = ids ? `https://api.figma.com/v1/files/${fileKey}/nodes?ids=${ids}` : `https://api.figma.com/v1/files/${fileKey}?depth=2`;
|
|
1106
|
+
const response = await fetch(url, {
|
|
1107
|
+
headers: { "X-Figma-Token": apiKey }
|
|
1108
|
+
});
|
|
1109
|
+
if (!response.ok) return [];
|
|
1110
|
+
const data = await response.json();
|
|
1111
|
+
const tokens = [];
|
|
1112
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1113
|
+
if (data.nodes) {
|
|
1114
|
+
for (const entry of Object.values(data.nodes)) {
|
|
1115
|
+
if (entry.document) walkNode(entry.document, tokens, seen);
|
|
1116
|
+
}
|
|
1117
|
+
} else if (data.document) {
|
|
1118
|
+
walkNode(data.document, tokens, seen);
|
|
1119
|
+
}
|
|
1120
|
+
return tokens;
|
|
1121
|
+
}
|
|
1122
|
+
function isFigmaApiKeyAvailable(projectRoot) {
|
|
1123
|
+
return readFigmaApiKeyFromMcp(projectRoot) !== null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/slash/build.ts
|
|
1127
|
+
function hasVisualDesignLinks(projectRoot, lang) {
|
|
1128
|
+
const agentsPath = path15.join(projectRoot, "AGENTS.md");
|
|
1129
|
+
if (!existsSync14(agentsPath)) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
return extractVisualDesignSection(readFileSync12(agentsPath, "utf-8"), lang).length > 0;
|
|
1133
|
+
}
|
|
1134
|
+
async function promptDesignSupplement(projectRoot, lang) {
|
|
1135
|
+
if (!isStdinTTY()) {
|
|
1136
|
+
console.log(t("build.supplementNonInteractive", lang));
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
console.log("");
|
|
1140
|
+
const { action } = await inquirer3.prompt([
|
|
1141
|
+
{
|
|
1142
|
+
type: "list",
|
|
1143
|
+
name: "action",
|
|
1144
|
+
message: t("build.supplementQuestion", lang),
|
|
1145
|
+
choices: [
|
|
1146
|
+
{ name: t("build.supplementApiKey", lang), value: "api_key" },
|
|
1147
|
+
{ name: t("build.supplementDesignLink", lang), value: "design_link" },
|
|
1148
|
+
{ name: t("build.supplementSkip", lang), value: "skip" }
|
|
1149
|
+
]
|
|
1150
|
+
}
|
|
1151
|
+
]);
|
|
1152
|
+
if (action === "skip") {
|
|
1153
|
+
console.log(chalk3.yellow(t("build.supplementSkipped", lang)));
|
|
1154
|
+
return void 0;
|
|
1155
|
+
}
|
|
1156
|
+
if (action === "api_key") {
|
|
1157
|
+
const { apiKey } = await inquirer3.prompt([
|
|
1158
|
+
{
|
|
1159
|
+
type: "password",
|
|
1160
|
+
name: "apiKey",
|
|
1161
|
+
message: t("init.figmaApiKeyQuestion", lang),
|
|
1162
|
+
mask: "*",
|
|
1163
|
+
validate: (value) => value.trim().length > 0 ? true : t("init.figmaApiKeyRequired", lang)
|
|
1164
|
+
}
|
|
1165
|
+
]);
|
|
1166
|
+
mergeFigmaMcpConfig(projectRoot, apiKey.trim());
|
|
1167
|
+
console.log(chalk3.green(tf("init.figmaConfigured", lang, { path: ".cursor/mcp.json" })));
|
|
1168
|
+
console.log(chalk3.yellow(t("build.supplementMcpRestartHint", lang)));
|
|
1169
|
+
return { enabled: true, configured: true };
|
|
1170
|
+
}
|
|
1171
|
+
const { link } = await inquirer3.prompt([
|
|
1172
|
+
{
|
|
1173
|
+
type: "input",
|
|
1174
|
+
name: "link",
|
|
1175
|
+
message: t("build.supplementDesignLinkQuestion", lang),
|
|
1176
|
+
validate: (value) => value.trim().length > 0 ? true : t("build.supplementDesignLinkRequired", lang)
|
|
1177
|
+
}
|
|
1178
|
+
]);
|
|
1179
|
+
console.log(chalk3.cyan(tf("build.supplementLinkNoted", lang, { link: link.trim() })));
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
async function resolveDesignTokens(projectRoot, designSection, figmaReady) {
|
|
1183
|
+
const links = extractFigmaLinks(designSection);
|
|
1184
|
+
const primary = links.find((l) => l.fileKey) ?? links[0];
|
|
1185
|
+
if (!figmaReady || !primary?.fileKey) {
|
|
1186
|
+
return { tokens: [], primaryLink: primary };
|
|
1187
|
+
}
|
|
1188
|
+
if (!isFigmaApiKeyAvailable(projectRoot)) {
|
|
1189
|
+
return { tokens: [], primaryLink: primary };
|
|
1190
|
+
}
|
|
1191
|
+
try {
|
|
1192
|
+
const tokens = await fetchFigmaDesignTokens(
|
|
1193
|
+
projectRoot,
|
|
1194
|
+
primary.fileKey,
|
|
1195
|
+
primary.nodeId
|
|
1196
|
+
);
|
|
1197
|
+
return { tokens, primaryLink: primary };
|
|
1198
|
+
} catch {
|
|
1199
|
+
return { tokens: [], primaryLink: primary };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function runBuildPrep(config) {
|
|
1203
|
+
const projectRoot = process.cwd();
|
|
1204
|
+
const lang = config.language;
|
|
1205
|
+
const figmaReady = isFigmaMcpConfigured(projectRoot) || config.figma.enabled;
|
|
1206
|
+
const hasDesignLinks = hasVisualDesignLinks(projectRoot, lang);
|
|
1207
|
+
const needsSupplement = !figmaReady || !hasDesignLinks;
|
|
1208
|
+
console.log(`# ${t("build.title", lang)}
|
|
1209
|
+
`);
|
|
1210
|
+
console.log(t("build.mandatory", lang));
|
|
1211
|
+
console.log("");
|
|
1212
|
+
console.log(`## ${t("build.stepFetch", lang)}
|
|
1213
|
+
`);
|
|
1214
|
+
console.log(t("build.stepFetchDetail", lang));
|
|
1215
|
+
console.log("");
|
|
1216
|
+
const agentsPath = path15.join(projectRoot, "AGENTS.md");
|
|
1217
|
+
let designSection = "";
|
|
1218
|
+
if (existsSync14(agentsPath)) {
|
|
1219
|
+
designSection = extractVisualDesignSection(readFileSync12(agentsPath, "utf-8"), lang);
|
|
1220
|
+
if (designSection) {
|
|
1221
|
+
console.log(`### ${t("build.designLinksFromAgents", lang)}
|
|
1222
|
+
`);
|
|
1223
|
+
console.log(designSection);
|
|
1224
|
+
console.log("");
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
console.log(`## ${t("build.figmaMcpStatus", lang)}
|
|
1228
|
+
`);
|
|
1229
|
+
if (figmaReady) {
|
|
1230
|
+
console.log(t("build.figmaMcpReady", lang));
|
|
1231
|
+
} else {
|
|
1232
|
+
console.log(t("build.figmaMcpMissing", lang));
|
|
1233
|
+
}
|
|
1234
|
+
console.log("");
|
|
1235
|
+
console.log(`## ${t("build.fidelityRules", lang)}
|
|
1236
|
+
`);
|
|
1237
|
+
console.log(t("build.fidelityRulesDetail", lang));
|
|
1238
|
+
console.log("");
|
|
1239
|
+
console.log(`## ${t("build.visualSpec", lang)}
|
|
1240
|
+
`);
|
|
1241
|
+
console.log(t("build.visualSpecDetail", lang));
|
|
1242
|
+
console.log("");
|
|
1243
|
+
const skillPath = getVisualFidelitySkillPath(projectRoot);
|
|
1244
|
+
const { tokens, primaryLink } = await resolveDesignTokens(
|
|
1245
|
+
projectRoot,
|
|
1246
|
+
designSection,
|
|
1247
|
+
figmaReady
|
|
1248
|
+
);
|
|
1249
|
+
const specResult = ensureVisualSpec(projectRoot, lang, designSection, tokens, primaryLink);
|
|
1250
|
+
const checklist = readVisualChecklist(projectRoot);
|
|
1251
|
+
const counts = countChecklistByStatus(checklist);
|
|
1252
|
+
console.log(tf("build.visualSpecWritten", lang, { path: specResult.specPath }));
|
|
1253
|
+
console.log(tf("build.visualChecklistWritten", lang, { path: specResult.checklistPath }));
|
|
1254
|
+
if (tokens.length > 0) {
|
|
1255
|
+
console.log(tf("build.figmaTokensExtracted", lang, { count: String(tokens.length) }));
|
|
1256
|
+
} else if (figmaReady && primaryLink?.fileKey) {
|
|
1257
|
+
console.log(t("build.figmaTokensEmpty", lang));
|
|
1258
|
+
}
|
|
1259
|
+
console.log(tf("build.visualSkillPath", lang, { path: skillPath }));
|
|
1260
|
+
console.log(tf("build.checklistSummary", lang, {
|
|
1261
|
+
passed: String(counts.passed),
|
|
1262
|
+
pending: String(counts.pending),
|
|
1263
|
+
total: String(counts.total)
|
|
1264
|
+
}));
|
|
1265
|
+
console.log("");
|
|
1266
|
+
let figmaUpdate;
|
|
1267
|
+
if (needsSupplement) {
|
|
1268
|
+
console.log(`## ${t("build.supplement", lang)}
|
|
1269
|
+
`);
|
|
1270
|
+
console.log(t("build.supplementDetail", lang));
|
|
1271
|
+
console.log("");
|
|
1272
|
+
figmaUpdate = await promptDesignSupplement(projectRoot, lang);
|
|
1273
|
+
}
|
|
1274
|
+
console.log(`## ${t("build.nextStep", lang)}
|
|
1275
|
+
`);
|
|
1276
|
+
console.log(t("build.nextStepDetail", lang));
|
|
1277
|
+
console.log("");
|
|
1278
|
+
console.log(t("build.agentMustReadSkill", lang));
|
|
1279
|
+
return figmaUpdate ? { figma: figmaUpdate } : {};
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
// src/slash/visual-regression-prep.ts
|
|
1283
|
+
import path16 from "path";
|
|
1284
|
+
import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
|
|
1285
|
+
async function runVisualRegressionPrep(config, mode) {
|
|
1286
|
+
const projectRoot = process.cwd();
|
|
1287
|
+
const lang = config.language;
|
|
1288
|
+
const prefix = mode;
|
|
1289
|
+
console.log(`# ${t(`${prefix}.title`, lang)}
|
|
1290
|
+
`);
|
|
1291
|
+
console.log(t(`${prefix}.regressionIntro`, lang));
|
|
1292
|
+
console.log("");
|
|
1293
|
+
const specPath = getVisualSpecPath(projectRoot);
|
|
1294
|
+
const checklistPath = getVisualChecklistPath(projectRoot);
|
|
1295
|
+
const skillPath = getVisualFidelitySkillPath(projectRoot);
|
|
1296
|
+
if (existsSync15(specPath)) {
|
|
1297
|
+
console.log(tf("regression.specPath", lang, { path: specPath }));
|
|
1298
|
+
} else {
|
|
1299
|
+
console.log(t("regression.specMissing", lang));
|
|
1300
|
+
}
|
|
1301
|
+
if (existsSync15(checklistPath)) {
|
|
1302
|
+
const checklist = readVisualChecklist(projectRoot);
|
|
1303
|
+
const counts = countChecklistByStatus(checklist);
|
|
1304
|
+
console.log(tf("regression.checklistSummary", lang, {
|
|
1305
|
+
passed: String(counts.passed),
|
|
1306
|
+
pending: String(counts.pending),
|
|
1307
|
+
failed: String(counts.failed),
|
|
1308
|
+
total: String(counts.total)
|
|
1309
|
+
}));
|
|
1310
|
+
const locked = checklist.items.filter((i) => i.status === "passed");
|
|
1311
|
+
if (locked.length > 0) {
|
|
1312
|
+
console.log("");
|
|
1313
|
+
console.log(`### ${t("regression.lockedItems", lang)}
|
|
1314
|
+
`);
|
|
1315
|
+
for (const item of locked) {
|
|
1316
|
+
console.log(`- \`${item.id}\`: ${item.description} (${item.designValue})`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} else {
|
|
1320
|
+
console.log(t("regression.checklistMissing", lang));
|
|
1321
|
+
}
|
|
1322
|
+
console.log("");
|
|
1323
|
+
console.log(tf("regression.skillPath", lang, { path: skillPath }));
|
|
1324
|
+
console.log("");
|
|
1325
|
+
console.log(`## ${t("regression.protocol", lang)}
|
|
1326
|
+
`);
|
|
1327
|
+
console.log(t("regression.protocolDetail", lang));
|
|
1328
|
+
console.log("");
|
|
1329
|
+
const agentsPath = path16.join(projectRoot, "AGENTS.md");
|
|
1330
|
+
if (existsSync15(agentsPath)) {
|
|
1331
|
+
const designSection = extractVisualDesignSection(readFileSync13(agentsPath, "utf-8"), lang);
|
|
1332
|
+
if (designSection) {
|
|
1333
|
+
console.log(`### ${t("regression.designContext", lang)}
|
|
1334
|
+
`);
|
|
1335
|
+
console.log(designSection);
|
|
1336
|
+
console.log("");
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
console.log(t(`${prefix}.agentMustReadSkill`, lang));
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/slash/tweak.ts
|
|
1343
|
+
async function runTweakPrep(config) {
|
|
1344
|
+
await runVisualRegressionPrep(config, "tweak");
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/slash/hotfix.ts
|
|
1348
|
+
async function runHotfixPrep(config) {
|
|
1349
|
+
await runVisualRegressionPrep(config, "hotfix");
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/slash/verify.ts
|
|
1353
|
+
import chalk4 from "chalk";
|
|
1354
|
+
import { existsSync as existsSync16 } from "fs";
|
|
1355
|
+
import path17 from "path";
|
|
1356
|
+
async function runVerifyPrep(config) {
|
|
1357
|
+
const projectRoot = process.cwd();
|
|
1358
|
+
const lang = config.language;
|
|
1359
|
+
console.log(`# ${t("verify.title", lang)}
|
|
1360
|
+
`);
|
|
1361
|
+
console.log(t("verify.intro", lang));
|
|
1362
|
+
console.log("");
|
|
1363
|
+
const checklistPath = getVisualChecklistPath(projectRoot);
|
|
1364
|
+
if (existsSync16(checklistPath)) {
|
|
1365
|
+
const checklist = readVisualChecklist(projectRoot);
|
|
1366
|
+
const counts = countChecklistByStatus(checklist);
|
|
1367
|
+
console.log(`## ${t("verify.checklistGate", lang)}
|
|
1368
|
+
`);
|
|
1369
|
+
console.log(tf("verify.checklistSummary", lang, {
|
|
1370
|
+
passed: String(counts.passed),
|
|
1371
|
+
pending: String(counts.pending),
|
|
1372
|
+
failed: String(counts.failed),
|
|
1373
|
+
total: String(counts.total)
|
|
1374
|
+
}));
|
|
1375
|
+
if (counts.failed > 0) {
|
|
1376
|
+
console.log(chalk4.yellow(t("verify.checklistFailed", lang)));
|
|
1377
|
+
}
|
|
1378
|
+
if (counts.pending > 0) {
|
|
1379
|
+
console.log(chalk4.yellow(t("verify.checklistPending", lang)));
|
|
1380
|
+
}
|
|
1381
|
+
console.log("");
|
|
1382
|
+
} else {
|
|
1383
|
+
console.log(chalk4.yellow(t("verify.checklistMissing", lang)));
|
|
1384
|
+
console.log("");
|
|
1385
|
+
}
|
|
1386
|
+
const configPath = ensureVisualVerifyConfig(projectRoot);
|
|
1387
|
+
const verifyConfig = readVisualVerifyConfig(projectRoot);
|
|
1388
|
+
console.log(`## ${t("verify.visualDiff", lang)}
|
|
1389
|
+
`);
|
|
1390
|
+
console.log(tf("verify.configPath", lang, { path: configPath }));
|
|
1391
|
+
if (!verifyConfig?.enabled) {
|
|
1392
|
+
console.log(t("verify.visualDiffDisabled", lang));
|
|
1393
|
+
console.log(t("verify.enableHint", lang));
|
|
1394
|
+
console.log(tf("verify.scriptPath", lang, { path: path17.join(getScriptsDir(), "visual-diff.mjs") }));
|
|
1395
|
+
console.log("");
|
|
1396
|
+
console.log(t("verify.nextStepDetail", lang));
|
|
1397
|
+
return { visualDiffFailed: false };
|
|
1398
|
+
}
|
|
1399
|
+
if (!hasVisualDiffDependencies(projectRoot)) {
|
|
1400
|
+
console.log(chalk4.yellow(t("verify.depsMissing", lang)));
|
|
1401
|
+
console.log(t("verify.depsInstall", lang));
|
|
1402
|
+
console.log(tf("verify.scriptPath", lang, { path: path17.join(getScriptsDir(), "visual-diff.mjs") }));
|
|
1403
|
+
console.log("");
|
|
1404
|
+
console.log(t("verify.nextStepDetail", lang));
|
|
1405
|
+
return { visualDiffFailed: false };
|
|
1406
|
+
}
|
|
1407
|
+
console.log(t("verify.screensConfigured", lang));
|
|
1408
|
+
console.log(formatVerifyConfigSummary(verifyConfig, lang));
|
|
1409
|
+
console.log("");
|
|
1410
|
+
console.log(t("verify.runningDiff", lang));
|
|
1411
|
+
const { exitCode, report, stdout } = await runVisualDiff(projectRoot);
|
|
1412
|
+
if (stdout) {
|
|
1413
|
+
console.log(stdout.split("\n").filter((line) => !line.startsWith("{")).join("\n"));
|
|
1414
|
+
}
|
|
1415
|
+
if (report) {
|
|
1416
|
+
console.log("");
|
|
1417
|
+
if (report.passed) {
|
|
1418
|
+
console.log(chalk4.green(t("verify.visualDiffPassed", lang)));
|
|
1419
|
+
} else {
|
|
1420
|
+
console.log(chalk4.red(t("verify.visualDiffFailed", lang)));
|
|
1421
|
+
for (const result of report.results.filter((r) => !r.passed)) {
|
|
1422
|
+
console.log(chalk4.red(` \u2717 ${result.id}: ${result.error ?? `${result.diffPercent}% diff`}`));
|
|
1423
|
+
if (result.diff) {
|
|
1424
|
+
console.log(chalk4.gray(` diff: ${result.diff}`));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
console.log("");
|
|
1430
|
+
console.log(t("verify.nextStepDetail", lang));
|
|
1431
|
+
console.log(t("verify.agentInstruction", lang));
|
|
1432
|
+
return { visualDiffFailed: exitCode !== 0 && verifyConfig.enabled };
|
|
1433
|
+
}
|
|
1434
|
+
|
|
690
1435
|
// src/core/rtk-bridge.ts
|
|
691
|
-
import { execa as
|
|
1436
|
+
import { execa as execa5 } from "execa";
|
|
692
1437
|
|
|
693
1438
|
// src/core/rtk.ts
|
|
694
1439
|
var HEAD_LINES = 200;
|
|
@@ -724,7 +1469,7 @@ async function runWithRtk(command, args, config, options = {}) {
|
|
|
724
1469
|
const ttyPassthrough = options.inheritStdio ?? false;
|
|
725
1470
|
const rtkOff = options.rtkDisabled || bypass || ttyPassthrough;
|
|
726
1471
|
if (ttyPassthrough) {
|
|
727
|
-
const subprocess =
|
|
1472
|
+
const subprocess = execa5(command, args, {
|
|
728
1473
|
cwd: options.cwd,
|
|
729
1474
|
stdio: "inherit",
|
|
730
1475
|
reject: false
|
|
@@ -736,7 +1481,7 @@ async function runWithRtk(command, args, config, options = {}) {
|
|
|
736
1481
|
exitCode: result2.exitCode ?? (result2.failed ? 1 : 0)
|
|
737
1482
|
};
|
|
738
1483
|
}
|
|
739
|
-
const result = await
|
|
1484
|
+
const result = await execa5(command, args, {
|
|
740
1485
|
cwd: options.cwd,
|
|
741
1486
|
reject: false,
|
|
742
1487
|
all: true
|
|
@@ -759,7 +1504,7 @@ ${result.stderr}`;
|
|
|
759
1504
|
}
|
|
760
1505
|
async function runCometPassthrough(subcommand, extraArgs, config, options = {}) {
|
|
761
1506
|
const args = [subcommand, ...extraArgs];
|
|
762
|
-
const isTTY = isStdoutTTY() &&
|
|
1507
|
+
const isTTY = isStdoutTTY() && isStdinTTY2();
|
|
763
1508
|
if (isTTY) {
|
|
764
1509
|
const result = await runWithRtk("comet", args, config, {
|
|
765
1510
|
...options,
|
|
@@ -784,12 +1529,12 @@ async function runCometPassthrough(subcommand, extraArgs, config, options = {})
|
|
|
784
1529
|
throw err;
|
|
785
1530
|
}
|
|
786
1531
|
}
|
|
787
|
-
function
|
|
1532
|
+
function isStdinTTY2() {
|
|
788
1533
|
return Boolean(process.stdin.isTTY);
|
|
789
1534
|
}
|
|
790
1535
|
|
|
791
1536
|
// src/slash/comet-passthrough.ts
|
|
792
|
-
import
|
|
1537
|
+
import chalk5 from "chalk";
|
|
793
1538
|
var COMET_MAP = {
|
|
794
1539
|
open: "open",
|
|
795
1540
|
design: "design",
|
|
@@ -807,74 +1552,124 @@ async function runCometSlash(subcommand, args, config) {
|
|
|
807
1552
|
} catch (err) {
|
|
808
1553
|
const message = err.message;
|
|
809
1554
|
if (message === "NON_INTERACTIVE_FAILED") {
|
|
810
|
-
console.error(
|
|
811
|
-
console.error(
|
|
1555
|
+
console.error(chalk5.red(t("error.cometNonInteractive", config.language)));
|
|
1556
|
+
console.error(chalk5.yellow(t("error.cometUseTerminal", config.language)));
|
|
812
1557
|
return 1;
|
|
813
1558
|
}
|
|
814
|
-
console.error(
|
|
1559
|
+
console.error(chalk5.red(String(err)));
|
|
815
1560
|
return 1;
|
|
816
1561
|
}
|
|
817
1562
|
}
|
|
818
1563
|
|
|
819
|
-
// src/slash/
|
|
820
|
-
import
|
|
821
|
-
async function
|
|
822
|
-
const
|
|
823
|
-
const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1564
|
+
// src/slash/sync-cursor-commands.ts
|
|
1565
|
+
import chalk6 from "chalk";
|
|
1566
|
+
async function runSyncCursorCommands() {
|
|
1567
|
+
const config = await loadConfig();
|
|
1568
|
+
const count = injectCursorSlashCommands();
|
|
1569
|
+
console.log(
|
|
1570
|
+
chalk6.green(tf("cursor.commandsSynced", config.language, { count: String(count) }))
|
|
1571
|
+
);
|
|
1572
|
+
const skillCount = injectVisualFidelitySkill();
|
|
1573
|
+
console.log(
|
|
1574
|
+
chalk6.green(tf("cursor.skillsSynced", config.language, { count: String(skillCount) }))
|
|
1575
|
+
);
|
|
1576
|
+
console.log(chalk6.cyan(t("cursor.commandsHint", config.language)));
|
|
832
1577
|
}
|
|
833
1578
|
|
|
834
|
-
// src/slash/
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
1579
|
+
// src/slash/visual-tools-install.ts
|
|
1580
|
+
import { existsSync as existsSync17 } from "fs";
|
|
1581
|
+
import path18 from "path";
|
|
1582
|
+
import chalk7 from "chalk";
|
|
1583
|
+
import { execa as execa6 } from "execa";
|
|
839
1584
|
|
|
840
|
-
// src/
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1585
|
+
// src/core/visual-tools.ts
|
|
1586
|
+
var VISUAL_DIFF_PACKAGES = ["playwright", "pixelmatch", "pngjs"];
|
|
1587
|
+
function getMissingVisualDiffDeps(projectRoot) {
|
|
1588
|
+
return VISUAL_DIFF_PACKAGES.filter((pkg) => !resolveProjectDependency(projectRoot, pkg));
|
|
1589
|
+
}
|
|
1590
|
+
function buildInstallDevDepsCommand(packageManager) {
|
|
1591
|
+
const packages = [...VISUAL_DIFF_PACKAGES];
|
|
1592
|
+
switch (packageManager) {
|
|
1593
|
+
case "pnpm":
|
|
1594
|
+
return { command: "pnpm", args: ["add", "-D", ...packages] };
|
|
1595
|
+
case "yarn":
|
|
1596
|
+
return { command: "yarn", args: ["add", "-D", ...packages] };
|
|
1597
|
+
case "bun":
|
|
1598
|
+
return { command: "bun", args: ["add", "-d", ...packages] };
|
|
1599
|
+
default:
|
|
1600
|
+
return { command: "npm", args: ["install", "-D", ...packages] };
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function buildPlaywrightInstallCommand(packageManager) {
|
|
1604
|
+
switch (packageManager) {
|
|
1605
|
+
case "pnpm":
|
|
1606
|
+
return { command: "pnpm", args: ["exec", "playwright", "install", "chromium"] };
|
|
1607
|
+
case "yarn":
|
|
1608
|
+
return { command: "yarn", args: ["exec", "playwright", "install", "chromium"] };
|
|
1609
|
+
case "bun":
|
|
1610
|
+
return { command: "bunx", args: ["playwright", "install", "chromium"] };
|
|
1611
|
+
default:
|
|
1612
|
+
return { command: "npx", args: ["playwright", "install", "chromium"] };
|
|
1613
|
+
}
|
|
844
1614
|
}
|
|
845
1615
|
|
|
846
|
-
// src/slash/
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const lang = config.language;
|
|
850
|
-
console.log(chalk5.bold(`## A. ${t("graph.handoffSummary", lang)}
|
|
851
|
-
`));
|
|
852
|
-
const summary = await runWithRtk("gitnexus", ["query", "--summary"], config);
|
|
853
|
-
if (summary.exitCode !== 0) {
|
|
854
|
-
console.log(chalk5.yellow(t("graph.summaryFailed", lang)));
|
|
855
|
-
}
|
|
856
|
-
console.log(chalk5.bold(`
|
|
857
|
-
## B. ${t("graph.handoffPrompt", lang)}
|
|
858
|
-
`));
|
|
859
|
-
console.log(t("graph.handoffPromptBody", lang));
|
|
860
|
-
console.log(chalk5.bold(`
|
|
861
|
-
## C. ${t("graph.handoffTimestamp", lang)}
|
|
862
|
-
`));
|
|
863
|
-
updateGraphTimestampInAgents();
|
|
864
|
-
console.log(chalk5.green(t("graph.timestampUpdated", lang)));
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// src/slash/graph-status.ts
|
|
868
|
-
import chalk6 from "chalk";
|
|
869
|
-
async function runGraphStatus(config) {
|
|
1616
|
+
// src/slash/visual-tools-install.ts
|
|
1617
|
+
async function runVisualToolsInstall(config) {
|
|
1618
|
+
const projectRoot = process.cwd();
|
|
870
1619
|
const lang = config.language;
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
console.log(
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1620
|
+
console.log(`# ${t("visualTools.title", lang)}
|
|
1621
|
+
`);
|
|
1622
|
+
console.log(t("visualTools.intro", lang));
|
|
1623
|
+
console.log("");
|
|
1624
|
+
const packageJsonPath = path18.join(projectRoot, "package.json");
|
|
1625
|
+
if (!existsSync17(packageJsonPath)) {
|
|
1626
|
+
console.error(chalk7.red(t("visualTools.noPackageJson", lang)));
|
|
1627
|
+
return 1;
|
|
1628
|
+
}
|
|
1629
|
+
const pm = detectPackageManager(projectRoot);
|
|
1630
|
+
console.log(tf("visualTools.packageManager", lang, { pm }));
|
|
1631
|
+
console.log("");
|
|
1632
|
+
const missing = getMissingVisualDiffDeps(projectRoot);
|
|
1633
|
+
if (missing.length === 0) {
|
|
1634
|
+
console.log(chalk7.green(t("visualTools.depsAlreadyInstalled", lang)));
|
|
1635
|
+
} else {
|
|
1636
|
+
const install = buildInstallDevDepsCommand(pm);
|
|
1637
|
+
console.log(tf("visualTools.installingDeps", lang, { packages: missing.join(", ") }));
|
|
1638
|
+
console.log(chalk7.gray(`$ ${[install.command, ...install.args].join(" ")}`));
|
|
1639
|
+
console.log("");
|
|
1640
|
+
const depsResult = await execa6(install.command, install.args, {
|
|
1641
|
+
cwd: projectRoot,
|
|
1642
|
+
stdio: "inherit",
|
|
1643
|
+
reject: false
|
|
1644
|
+
});
|
|
1645
|
+
if (depsResult.exitCode !== 0) {
|
|
1646
|
+
console.error(chalk7.red(t("visualTools.depsInstallFailed", lang)));
|
|
1647
|
+
return depsResult.exitCode ?? 1;
|
|
1648
|
+
}
|
|
1649
|
+
console.log(chalk7.green(t("visualTools.depsInstalled", lang)));
|
|
1650
|
+
console.log("");
|
|
1651
|
+
}
|
|
1652
|
+
const browserInstall = buildPlaywrightInstallCommand(pm);
|
|
1653
|
+
console.log(t("visualTools.installingBrowsers", lang));
|
|
1654
|
+
console.log(chalk7.gray(`$ ${[browserInstall.command, ...browserInstall.args].join(" ")}`));
|
|
1655
|
+
console.log("");
|
|
1656
|
+
const browserResult = await execa6(browserInstall.command, browserInstall.args, {
|
|
1657
|
+
cwd: projectRoot,
|
|
1658
|
+
stdio: "inherit",
|
|
1659
|
+
reject: false
|
|
1660
|
+
});
|
|
1661
|
+
if (browserResult.exitCode !== 0) {
|
|
1662
|
+
console.error(chalk7.red(t("visualTools.browsersInstallFailed", lang)));
|
|
1663
|
+
return browserResult.exitCode ?? 1;
|
|
1664
|
+
}
|
|
1665
|
+
if (!hasVisualDiffDependencies(projectRoot)) {
|
|
1666
|
+
console.error(chalk7.red(t("visualTools.verifyFailed", lang)));
|
|
1667
|
+
return 1;
|
|
1668
|
+
}
|
|
1669
|
+
console.log(chalk7.green.bold(t("visualTools.success", lang)));
|
|
1670
|
+
console.log("");
|
|
1671
|
+
console.log(t("visualTools.nextStep", lang));
|
|
1672
|
+
return 0;
|
|
878
1673
|
}
|
|
879
1674
|
|
|
880
1675
|
// src/cli/commands/slash.ts
|
|
@@ -887,27 +1682,21 @@ var COMET_COMMANDS = [
|
|
|
887
1682
|
"hotfix",
|
|
888
1683
|
"tweak"
|
|
889
1684
|
];
|
|
890
|
-
var GRAPH_COMMANDS = [
|
|
891
|
-
"graph-setup",
|
|
892
|
-
"graph-init",
|
|
893
|
-
"graph-refresh",
|
|
894
|
-
"graph-handoff",
|
|
895
|
-
"graph-status"
|
|
896
|
-
];
|
|
897
1685
|
var ALL_COMMANDS = [
|
|
898
1686
|
"fill-context",
|
|
899
1687
|
...COMET_COMMANDS,
|
|
900
|
-
|
|
1688
|
+
"sync-cursor-commands",
|
|
1689
|
+
"visual-tools-install"
|
|
901
1690
|
];
|
|
902
1691
|
function gateCometCommands(projectRoot, lang) {
|
|
903
1692
|
if (agentsMdHasPendingPlaceholders(projectRoot)) {
|
|
904
|
-
console.error(
|
|
1693
|
+
console.error(chalk8.yellow(t("gate.fillContextFirst", lang)));
|
|
905
1694
|
process.exit(1);
|
|
906
1695
|
}
|
|
907
1696
|
}
|
|
908
1697
|
async function runSlash(command, args) {
|
|
909
1698
|
if (!ALL_COMMANDS.includes(command)) {
|
|
910
|
-
console.error(
|
|
1699
|
+
console.error(chalk8.red(`Unknown slash command: ${command}`));
|
|
911
1700
|
process.exit(1);
|
|
912
1701
|
}
|
|
913
1702
|
const projectRoot = process.cwd();
|
|
@@ -916,28 +1705,44 @@ async function runSlash(command, args) {
|
|
|
916
1705
|
const slashCmd = command;
|
|
917
1706
|
if (COMET_COMMANDS.includes(slashCmd)) {
|
|
918
1707
|
gateCometCommands(projectRoot, lang);
|
|
1708
|
+
if (slashCmd === "build") {
|
|
1709
|
+
const prepResult = await runBuildPrep(config);
|
|
1710
|
+
if (prepResult.figma) {
|
|
1711
|
+
writeDefaultConfig(projectRoot, mergeConfig({ ...config, figma: prepResult.figma }));
|
|
1712
|
+
}
|
|
1713
|
+
console.log("");
|
|
1714
|
+
}
|
|
1715
|
+
if (slashCmd === "tweak") {
|
|
1716
|
+
await runTweakPrep(config);
|
|
1717
|
+
console.log("");
|
|
1718
|
+
}
|
|
1719
|
+
if (slashCmd === "hotfix") {
|
|
1720
|
+
await runHotfixPrep(config);
|
|
1721
|
+
console.log("");
|
|
1722
|
+
}
|
|
1723
|
+
let visualDiffFailed = false;
|
|
1724
|
+
if (slashCmd === "verify") {
|
|
1725
|
+
const verifyResult = await runVerifyPrep(config);
|
|
1726
|
+
visualDiffFailed = verifyResult.visualDiffFailed;
|
|
1727
|
+
console.log("");
|
|
1728
|
+
}
|
|
919
1729
|
const exitCode = await runCometSlash(slashCmd, args, config);
|
|
1730
|
+
if (visualDiffFailed && exitCode === 0) {
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
920
1733
|
process.exit(exitCode);
|
|
921
1734
|
}
|
|
922
1735
|
switch (slashCmd) {
|
|
923
1736
|
case "fill-context":
|
|
924
1737
|
await runFillContext(config);
|
|
925
1738
|
break;
|
|
926
|
-
case "
|
|
927
|
-
await
|
|
928
|
-
break;
|
|
929
|
-
case "graph-init":
|
|
930
|
-
await runGraphInit(config);
|
|
931
|
-
break;
|
|
932
|
-
case "graph-refresh":
|
|
933
|
-
await runGraphRefresh(config);
|
|
934
|
-
break;
|
|
935
|
-
case "graph-handoff":
|
|
936
|
-
await runGraphHandoff(config);
|
|
937
|
-
break;
|
|
938
|
-
case "graph-status":
|
|
939
|
-
await runGraphStatus(config);
|
|
1739
|
+
case "sync-cursor-commands":
|
|
1740
|
+
await runSyncCursorCommands();
|
|
940
1741
|
break;
|
|
1742
|
+
case "visual-tools-install": {
|
|
1743
|
+
const exitCode = await runVisualToolsInstall(config);
|
|
1744
|
+
process.exit(exitCode);
|
|
1745
|
+
}
|
|
941
1746
|
default:
|
|
942
1747
|
process.exit(1);
|
|
943
1748
|
}
|
|
@@ -945,14 +1750,14 @@ async function runSlash(command, args) {
|
|
|
945
1750
|
|
|
946
1751
|
// src/cli/index.ts
|
|
947
1752
|
var program = new Command();
|
|
948
|
-
program.name("ft").description("Frontend Toolkit \u2014 orchestration layer for Comet and
|
|
1753
|
+
program.name("ft").description("Frontend Toolkit \u2014 orchestration layer for Comet and Karpathy rules").version("0.1.0");
|
|
949
1754
|
program.command("init").description("Full project initialization pipeline").option("--lang <lang>", "Language: zh-CN or en").action(async (options) => {
|
|
950
1755
|
await runInit({ lang: options.lang });
|
|
951
1756
|
});
|
|
952
1757
|
program.command("update").description("Update @nick848/ft globally").action(async () => {
|
|
953
1758
|
await runUpdate();
|
|
954
1759
|
});
|
|
955
|
-
program.command("version").description("Show FT
|
|
1760
|
+
program.command("version").description("Show FT and Comet versions").action(async () => {
|
|
956
1761
|
await runVersion();
|
|
957
1762
|
});
|
|
958
1763
|
program.command("help").description("Show help").action(() => {
|