@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/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 inquirer from "inquirer";
9
- import { execa as execa2 } from "execa";
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, readdirSync } from "fs";
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
- tools: ["cursor", "codex", "opencode"],
175
- gitnexus: {
176
- graph_path: ".gitnexus/",
177
- auto_prompt_in_agents: true
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
- gitnexus: { ...DEFAULT_CONFIG.gitnexus, ...partial?.gitnexus },
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 getGitNexusCondition(lang) {
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, lang);
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
- readFileSync as readFileSync6,
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/mcp-setup.ts
434
- import chalk from "chalk";
435
- import readline from "readline";
436
- async function configureMcpForAdapters(adapterList, lang) {
437
- for (const adapter of adapterList) {
438
- const result = await adapter.configureMcp();
439
- if (result.success) {
440
- console.log(chalk.green(tf("mcp.writeSuccess", lang, { path: result.path })));
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
- await promptManualMcpConfig(adapter, lang, result.path, result.snippet);
443
- }
444
- }
445
- }
446
- async function promptManualMcpConfig(_adapter, lang, configPath, snippet) {
447
- const border = "\u2550".repeat(60);
448
- console.log(chalk.yellow(`
449
- ${border}`));
450
- console.log(chalk.yellow.bold(tf("mcp.manualTitle", lang, { path: configPath })));
451
- console.log(chalk.cyan(snippet));
452
- console.log(chalk.yellow(`${border}
453
- `));
454
- console.log(chalk.gray(tf("mcp.pressKey", lang)));
455
- await new Promise((resolve) => {
456
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
457
- process.stdin.setRawMode?.(true);
458
- process.stdin.resume();
459
- process.stdin.once("data", () => {
460
- rl.close();
461
- process.stdin.setRawMode?.(false);
462
- resolve();
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 inquirer.prompt([
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 inquirer.prompt([
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 (gitnexusInstalled) {
533
- await configureMcpForAdapters(adapterList, language);
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 execa2("comet", ["init"], { stdio: "inherit", cwd: projectRoot });
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 execa3 } from "execa";
833
+ import { execa as execa4 } from "execa";
552
834
  async function runUpdate() {
553
- await execa3("npm", ["update", "-g", "@nick848/ft"], { stdio: "inherit" });
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 readFileSync7 } from "fs";
558
- import path7 from "path";
839
+ import { readFileSync as readFileSync9 } from "fs";
840
+ import path12 from "path";
559
841
  function getFtVersion() {
560
842
  try {
561
- const pkgPath = path7.join(getPackageRoot(), "package.json");
562
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
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
- const gitnexusVersion = await detectGitNexus();
572
- console.log(`FT: ${ftVersion}`);
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, Karpathy rules, and GitNexus
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, Comet, and GitNexus versions
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 Output prompt recipe to fill AGENTS.md
591
- open Comet open (passthrough)
592
- design Comet design (passthrough)
593
- build Comet build (passthrough)
594
- verify Comet verify (passthrough)
595
- archive Comet archive (passthrough)
596
- hotfix Comet hotfix (passthrough)
597
- tweak Comet tweak (passthrough)
598
- graph-setup GitNexus install guide + MCP retry
599
- graph-init gitnexus analyze
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 chalk7 from "chalk";
884
+ import chalk8 from "chalk";
608
885
 
609
886
  // src/slash/fill-context.ts
610
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
611
- import path8 from "path";
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 = path8.join(projectRoot, "AGENTS.md");
933
+ const agentsPath = path13.join(projectRoot, "AGENTS.md");
645
934
  const lang = config.language;
646
- if (!existsSync7(agentsPath)) {
935
+ if (!existsSync12(agentsPath)) {
647
936
  console.log(t("fillContext.noAgents", lang));
648
937
  return;
649
938
  }
650
- const agentsContent = readFileSync8(agentsPath, "utf-8");
939
+ const agentsContent = readFileSync10(agentsPath, "utf-8");
651
940
  const placeholders = extractSections(agentsContent);
652
- const backupPath = path8.join(projectRoot, "AGENTS-BAK.md");
653
- const backupContent = existsSync7(backupPath) ? readFileSync8(backupPath, "utf-8") : "";
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 execa4 } from "execa";
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 = execa4(command, args, {
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 execa4(command, args, {
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() && isStdinTTY();
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 isStdinTTY() {
1532
+ function isStdinTTY2() {
788
1533
  return Boolean(process.stdin.isTTY);
789
1534
  }
790
1535
 
791
1536
  // src/slash/comet-passthrough.ts
792
- import chalk3 from "chalk";
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(chalk3.red(t("error.cometNonInteractive", config.language)));
811
- console.error(chalk3.yellow(t("error.cometUseTerminal", config.language)));
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(chalk3.red(String(err)));
1559
+ console.error(chalk5.red(String(err)));
815
1560
  return 1;
816
1561
  }
817
1562
  }
818
1563
 
819
- // src/slash/graph-setup.ts
820
- import chalk4 from "chalk";
821
- async function runGraphSetup(config) {
822
- const lang = config.language;
823
- const installed = await detectGitNexus();
824
- if (!installed) {
825
- console.log(chalk4.yellow(t("graph.setupGuide", lang)));
826
- console.log(chalk4.cyan("npm install -g gitnexus"));
827
- } else {
828
- console.log(chalk4.green(t("graph.alreadyInstalled", lang)));
829
- }
830
- const adapterList = getAdaptersForPlatforms(config.tools);
831
- await configureMcpForAdapters(adapterList, lang);
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/graph-init.ts
835
- async function runGraphInit(config) {
836
- const result = await runWithRtk("gitnexus", ["analyze"], config);
837
- process.exit(result.exitCode);
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/slash/graph-refresh.ts
841
- async function runGraphRefresh(config) {
842
- const result = await runWithRtk("gitnexus", ["analyze", "--force"], config);
843
- process.exit(result.exitCode);
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/graph-handoff.ts
847
- import chalk5 from "chalk";
848
- async function runGraphHandoff(config) {
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
- const version = await detectGitNexus();
872
- const graphExists = gitNexusGraphExists();
873
- const indexCount = countGitNexusIndexFiles();
874
- console.log(chalk6.bold(t("graph.statusTitle", lang)));
875
- console.log(`${t("graph.statusInstalled", lang)}: ${version ? chalk6.green(version) : chalk6.red("no")}`);
876
- console.log(`${t("graph.statusGraph", lang)}: ${graphExists ? chalk6.green("yes") : chalk6.yellow("no")}`);
877
- console.log(`${t("graph.statusIndexFiles", lang)}: ${indexCount}`);
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
- ...GRAPH_COMMANDS
1688
+ "sync-cursor-commands",
1689
+ "visual-tools-install"
901
1690
  ];
902
1691
  function gateCometCommands(projectRoot, lang) {
903
1692
  if (agentsMdHasPendingPlaceholders(projectRoot)) {
904
- console.error(chalk7.yellow(t("gate.fillContextFirst", lang)));
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(chalk7.red(`Unknown slash command: ${command}`));
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 "graph-setup":
927
- await runGraphSetup(config);
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 GitNexus").version("0.1.0");
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, Comet, and GitNexus versions").action(async () => {
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(() => {