@atomixstudio/mcp 1.0.23 → 1.0.25

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
@@ -1480,15 +1480,6 @@ function figmaColorNameWithGroup(key) {
1480
1480
  const groupDisplay = group.charAt(0).toUpperCase() + group.slice(1).toLowerCase();
1481
1481
  return `${groupDisplay} / ${name}`;
1482
1482
  }
1483
- var FIGMA_COLOR_GROUP_ORDER = {
1484
- background: 0,
1485
- text: 1,
1486
- border: 2,
1487
- icon: 3,
1488
- brand: 4,
1489
- action: 5,
1490
- feedback: 6
1491
- };
1492
1483
  var FIGMA_SHADOW_ORDER = {
1493
1484
  none: 0,
1494
1485
  xs: 1,
@@ -1582,8 +1573,11 @@ function buildFigmaPayloadsFromDS(data) {
1582
1573
  if (Object.keys(light).length > 0) modes.push("Light");
1583
1574
  if (Object.keys(dark).length > 0 && !modes.includes("Dark")) modes.push("Dark");
1584
1575
  if (modes.length === 0) modes.push("Light");
1585
- const allKeys = /* @__PURE__ */ new Set([...Object.keys(light), ...Object.keys(dark)]);
1586
- for (const key of allKeys) {
1576
+ const orderedKeys = [...Object.keys(light)];
1577
+ for (const k of Object.keys(dark)) {
1578
+ if (!orderedKeys.includes(k)) orderedKeys.push(k);
1579
+ }
1580
+ for (const key of orderedKeys) {
1587
1581
  const lightHex = light[key];
1588
1582
  const darkHex = dark[key];
1589
1583
  if (typeof lightHex === "string" && hexRe.test(lightHex)) {
@@ -1612,26 +1606,8 @@ function buildFigmaPayloadsFromDS(data) {
1612
1606
  }
1613
1607
  }
1614
1608
  if (variables.length === 0 && modes.length === 0) modes.push("Light");
1615
- const colorSortKey = (figmaName) => {
1616
- const slash = figmaName.indexOf(" / ");
1617
- const group = slash >= 0 ? figmaName.slice(0, slash).toLowerCase() : "";
1618
- const name = slash >= 0 ? figmaName.slice(slash + 3) : figmaName;
1619
- const order = FIGMA_COLOR_GROUP_ORDER[group] ?? 999;
1620
- return [order, name];
1621
- };
1622
- variables.sort((a, b) => {
1623
- const [oA, nA] = colorSortKey(a.name);
1624
- const [oB, nB] = colorSortKey(b.name);
1625
- if (oA !== oB) return oA - oB;
1626
- return nA.localeCompare(nB);
1627
- });
1628
- paintStyles.sort((a, b) => {
1629
- const [oA, nA] = colorSortKey(a.name);
1630
- const [oB, nB] = colorSortKey(b.name);
1631
- if (oA !== oB) return oA - oB;
1632
- return nA.localeCompare(nB);
1633
- });
1634
- const collectionName = data.meta?.name ? `${data.meta.name} Colors` : "Atomix Colors";
1609
+ const dsName = data.meta?.name;
1610
+ const collectionName = dsName ? `${dsName} Foundations` : "Foundations";
1635
1611
  const textStyles = [];
1636
1612
  const sizeToPx = (val, basePx = 16) => {
1637
1613
  if (typeof val === "number") return Math.round(val);
@@ -1735,7 +1711,6 @@ function buildFigmaPayloadsFromDS(data) {
1735
1711
  if (a.fontSize !== b.fontSize) return a.fontSize - b.fontSize;
1736
1712
  return (a.name || "").localeCompare(b.name || "");
1737
1713
  });
1738
- const dsName = data.meta?.name ?? "Atomix";
1739
1714
  const numberVariableCollections = [];
1740
1715
  const spacing = tokens?.spacing;
1741
1716
  if (spacing?.scale && typeof spacing.scale === "object") {
@@ -1745,7 +1720,7 @@ function buildFigmaPayloadsFromDS(data) {
1745
1720
  if (n >= 0) variables2.push({ name: key, value: n });
1746
1721
  }
1747
1722
  variables2.sort((a, b) => a.value - b.value);
1748
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Spacing`, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1723
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Spacing", variables: variables2, scopes: ["GAP"] });
1749
1724
  }
1750
1725
  const radius = tokens?.radius;
1751
1726
  if (radius?.scale && typeof radius.scale === "object") {
@@ -1755,7 +1730,7 @@ function buildFigmaPayloadsFromDS(data) {
1755
1730
  if (n >= 0) variables2.push({ name: key, value: n });
1756
1731
  }
1757
1732
  variables2.sort((a, b) => a.value - b.value);
1758
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Radius`, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1733
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Radius", variables: variables2, scopes: ["CORNER_RADIUS"] });
1759
1734
  }
1760
1735
  const borders = tokens?.borders;
1761
1736
  if (borders?.width && typeof borders.width === "object") {
@@ -1765,7 +1740,7 @@ function buildFigmaPayloadsFromDS(data) {
1765
1740
  if (n >= 0) variables2.push({ name: key, value: n });
1766
1741
  }
1767
1742
  variables2.sort((a, b) => a.value - b.value);
1768
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Borders`, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1743
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Borders", variables: variables2, scopes: ["STROKE_FLOAT"] });
1769
1744
  }
1770
1745
  const sizing = tokens?.sizing;
1771
1746
  if (sizing?.height && typeof sizing.height === "object") {
@@ -1775,7 +1750,7 @@ function buildFigmaPayloadsFromDS(data) {
1775
1750
  if (n >= 0) variables2.push({ name: key, value: n });
1776
1751
  }
1777
1752
  variables2.sort((a, b) => a.value - b.value);
1778
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Height`, categoryKey: "Height", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1753
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Height", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1779
1754
  }
1780
1755
  if (sizing?.icon && typeof sizing.icon === "object") {
1781
1756
  const variables2 = [];
@@ -1784,7 +1759,7 @@ function buildFigmaPayloadsFromDS(data) {
1784
1759
  if (n >= 0) variables2.push({ name: key, value: n });
1785
1760
  }
1786
1761
  variables2.sort((a, b) => a.value - b.value);
1787
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Icon`, categoryKey: "Icon", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1762
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Icon", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1788
1763
  }
1789
1764
  const layout = tokens?.layout;
1790
1765
  if (layout?.breakpoint && typeof layout.breakpoint === "object") {
@@ -1794,7 +1769,7 @@ function buildFigmaPayloadsFromDS(data) {
1794
1769
  if (n >= 0) variables2.push({ name: key, value: n });
1795
1770
  }
1796
1771
  variables2.sort((a, b) => a.value - b.value);
1797
- if (variables2.length > 0) numberVariableCollections.push({ collectionName: `${dsName} Breakpoint`, categoryKey: "Breakpoint", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1772
+ if (variables2.length > 0) numberVariableCollections.push({ collectionName, categoryKey: "Breakpoint", variables: variables2, scopes: ["WIDTH_HEIGHT"] });
1798
1773
  }
1799
1774
  const effectStyles = [];
1800
1775
  const shadows = tokens?.shadows;
@@ -1909,6 +1884,43 @@ ${changes.summary}`);
1909
1884
  }
1910
1885
  }
1911
1886
  }
1887
+ function validateTokenFileAfterWrite(outputPath, format, expectedMinVariables) {
1888
+ if (!fs2.existsSync(outputPath)) {
1889
+ return { path: outputPath, status: "FAIL", detail: "File not found after write." };
1890
+ }
1891
+ const content = fs2.readFileSync(outputPath, "utf-8");
1892
+ if (!content || content.trim().length === 0) {
1893
+ return { path: outputPath, status: "FAIL", detail: "File is empty after write." };
1894
+ }
1895
+ if (["css", "scss", "less"].includes(format)) {
1896
+ const varPattern = /(--[a-zA-Z0-9-]+):\s*[^;]+;/g;
1897
+ let count = 0;
1898
+ let m;
1899
+ while ((m = varPattern.exec(content)) !== null) count++;
1900
+ if (count < expectedMinVariables) {
1901
+ return {
1902
+ path: outputPath,
1903
+ status: "FAIL",
1904
+ detail: `Expected at least ${expectedMinVariables} variables; found ${count}.`
1905
+ };
1906
+ }
1907
+ return { path: outputPath, status: "OK", detail: `${count} variables written.` };
1908
+ }
1909
+ return { path: outputPath, status: "OK", detail: "File written (non-CSS format)." };
1910
+ }
1911
+ function formatValidationBlock(entries) {
1912
+ if (entries.length === 0) return "";
1913
+ const displayPath = (p) => p.startsWith("(") ? p : path2.relative(process.cwd(), p);
1914
+ const lines = [
1915
+ "",
1916
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
1917
+ "VALIDATION (confirm sync succeeded)",
1918
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
1919
+ ...entries.map((e) => ` ${displayPath(e.path)}: ${e.status} \u2014 ${e.detail}`),
1920
+ ""
1921
+ ];
1922
+ return lines.join("\n");
1923
+ }
1912
1924
  async function fetchDesignSystemForMCP(forceRefresh = false) {
1913
1925
  if (!dsId) throw new Error("Missing --ds-id. Usage: npx @atomixstudio/mcp --ds-id <id> --atomix-token <token>");
1914
1926
  if (!accessToken) throw new Error("Missing --atomix-token. Get your token from the Export modal or Settings.");
@@ -1928,10 +1940,50 @@ async function fetchDesignSystemForMCP(forceRefresh = false) {
1928
1940
  return result.data;
1929
1941
  }
1930
1942
  var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
1943
+ function typesetKeyToFontFamilyRole(key) {
1944
+ const prefix = key.split("-")[0] ?? "";
1945
+ if (prefix === "display") return "display";
1946
+ if (prefix === "heading") return "heading";
1947
+ if (prefix === "mono") return "mono";
1948
+ if (prefix.startsWith("body")) return "body";
1949
+ return "body";
1950
+ }
1951
+ function buildTypesetsList(typography, cssPrefix = "atmx") {
1952
+ const fontSize = typography.fontSize;
1953
+ if (!fontSize || typeof fontSize !== "object") return [];
1954
+ const fontFamily = typography.fontFamily ?? {};
1955
+ const fontWeight = typography.fontWeight ?? {};
1956
+ const lineHeight = typography.lineHeight ?? {};
1957
+ const letterSpacing = typography.letterSpacing ?? {};
1958
+ const textTransform = typography.textTransform ?? {};
1959
+ const textDecoration = typography.textDecoration ?? {};
1960
+ const p = cssPrefix ? `${cssPrefix}-` : "";
1961
+ const typesets = [];
1962
+ for (const key of Object.keys(fontSize)) {
1963
+ const role = typesetKeyToFontFamilyRole(key);
1964
+ const familyName = fontFamily[role] ?? fontFamily.body;
1965
+ const fontFamilyVar = familyName ? `var(--${p}typography-font-family-${role})` : "";
1966
+ const keyKebab = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1967
+ typesets.push({
1968
+ key,
1969
+ cssClass: `.typeset-${keyKebab}`,
1970
+ fontFamilyVar: fontFamilyVar || "inherit",
1971
+ fontSizeVar: `var(--${p}typography-${key}-size)`,
1972
+ fontWeightVar: `var(--${p}typography-${key}-weight)`,
1973
+ lineHeightVar: `var(--${p}typography-${key}-line-height)`,
1974
+ letterSpacingVar: letterSpacing[key] != null ? `var(--${p}typography-${key}-letter-spacing)` : void 0,
1975
+ textTransformVar: textTransform[key] != null ? `var(--${p}typography-${key}-text-transform)` : void 0,
1976
+ textDecorationVar: textDecoration[key] != null ? `var(--${p}typography-${key}-text-decoration)` : void 0,
1977
+ hasTextTransform: key in textTransform,
1978
+ hasTextDecoration: key in textDecoration
1979
+ });
1980
+ }
1981
+ return typesets;
1982
+ }
1931
1983
  var server = new Server(
1932
1984
  {
1933
1985
  name: "atomix-mcp-user",
1934
- version: "1.0.22"
1986
+ version: "1.0.25"
1935
1987
  },
1936
1988
  {
1937
1989
  capabilities: {
@@ -2017,6 +2069,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2017
2069
  required: ["query"]
2018
2070
  }
2019
2071
  },
2072
+ {
2073
+ name: "listTypesets",
2074
+ description: "List every typography typeset in the design system with full CSS variable names. Use this when building typeset.css so you emit one class per typeset and include all properties (font-family, font-size, font-weight, line-height, letter-spacing, text-transform, text-decoration) for 1:1 match. Do not skip any typesets.",
2075
+ inputSchema: {
2076
+ type: "object",
2077
+ properties: {
2078
+ cssPrefix: {
2079
+ type: "string",
2080
+ description: "CSS variable prefix (default: atmx). Use the same prefix as the synced tokens file."
2081
+ }
2082
+ },
2083
+ required: []
2084
+ }
2085
+ },
2020
2086
  {
2021
2087
  name: "validateUsage",
2022
2088
  description: "Check if a CSS value follows the design system. Detects hardcoded values that should use tokens.",
@@ -2083,7 +2149,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2083
2149
  },
2084
2150
  {
2085
2151
  name: "syncAll",
2086
- description: "Sync tokens, AI rules, skills files (SKILL.md, design-in-figma.md), and atomix-dependencies.json. One tool for full project sync. Use /--sync prompt or call when the user wants to sync. Optional: output (default ./tokens.css), format (default css), skipTokens (if true, only writes skills and manifest).",
2152
+ description: "Sync tokens, AI rules, skills (SKILL.md, FIGMA-SKILL.md), and atomix-dependencies.json. Use dryRun: true first to report what would change without writing; then dryRun: false to apply. Response includes a VALIDATION section\u2014agent must check it to confirm success. Optional: output (default ./tokens.css), format (default css), skipTokens (if true, only skills and manifest), dryRun (if true, report only; no files written).",
2087
2153
  inputSchema: {
2088
2154
  type: "object",
2089
2155
  properties: {
@@ -2099,6 +2165,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2099
2165
  skipTokens: {
2100
2166
  type: "boolean",
2101
2167
  description: "If true, skip token file and rules sync; only write skills and dependencies manifest. Default: false."
2168
+ },
2169
+ dryRun: {
2170
+ type: "boolean",
2171
+ description: "If true, compute and return what would be written (paths, token counts, diff summary) but do NOT write any files. Call with dryRun: false to apply. Default: false."
2102
2172
  }
2103
2173
  },
2104
2174
  required: []
@@ -2311,7 +2381,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2311
2381
  isError: true
2312
2382
  };
2313
2383
  }
2314
- async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat) {
2384
+ async function performTokenSyncAndRules(designSystemData, tokenOutput, tokenFormat, dryRun) {
2315
2385
  const output = tokenOutput;
2316
2386
  const format = tokenFormat;
2317
2387
  const outputPath = path2.resolve(process.cwd(), output);
@@ -2374,7 +2444,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2374
2444
  const lightChanges = diff.added.length + diff.modified.length;
2375
2445
  const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
2376
2446
  const totalChanges = lightChanges + darkChanges + deprecatedCount;
2377
- if (totalChanges === 0) {
2447
+ if (totalChanges === 0 && !dryRun) {
2378
2448
  const lastUpdated = designSystemData.meta.exportedAt ? new Date(designSystemData.meta.exportedAt).toLocaleString() : "N/A";
2379
2449
  return {
2380
2450
  responseText: `\u2713 Already up to date!
@@ -2383,7 +2453,19 @@ File: ${output}
2383
2453
  Tokens: ${tokenCount}
2384
2454
  Version: ${designSystemData.meta.version}
2385
2455
  Last updated: ${lastUpdated}`,
2386
- rulesResults: []
2456
+ rulesResults: [],
2457
+ validation: [{ path: outputPath, status: "OK", detail: "No changes needed; file already up to date." }]
2458
+ };
2459
+ }
2460
+ if (totalChanges === 0 && dryRun) {
2461
+ return {
2462
+ responseText: `[DRY RUN] Already up to date. No changes would be written.
2463
+
2464
+ File: ${output}
2465
+ Tokens: ${tokenCount}
2466
+ Version: ${designSystemData.meta.version}`,
2467
+ rulesResults: [],
2468
+ validation: [{ path: "(dry run)", status: "OK", detail: "No files written." }]
2387
2469
  };
2388
2470
  }
2389
2471
  changes = [...diff.modified, ...diff.modifiedDark];
@@ -2397,9 +2479,34 @@ Last updated: ${lastUpdated}`,
2397
2479
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
2398
2480
  };
2399
2481
  }
2482
+ if (dryRun) {
2483
+ const added = diff ? diff.added.length + diff.addedDark.length : 0;
2484
+ const modified = diff ? diff.modified.length + diff.modifiedDark.length : 0;
2485
+ const changeLine = fileExists && diff ? ` Changes: ${added} added, ${modified} modified` : !fileExists ? ` New file would be created with ${tokenCount} tokens.` : "";
2486
+ const report = [
2487
+ "[DRY RUN] No files were written. The following would be written if you run syncAll with dryRun: false.",
2488
+ "",
2489
+ "Token file:",
2490
+ ` Path: ${output}`,
2491
+ ` Format: ${format}`,
2492
+ ` Tokens: ${tokenCount} (${deprecatedCount} deprecated preserved)`,
2493
+ changeLine,
2494
+ "",
2495
+ "Rules: .cursorrules (or existing rules files in project)",
2496
+ "",
2497
+ "Call syncAll again with dryRun: false to apply."
2498
+ ].filter(Boolean).join("\n");
2499
+ return {
2500
+ responseText: report,
2501
+ rulesResults: [],
2502
+ validation: [{ path: "(dry run)", status: "OK", detail: "No files written." }]
2503
+ };
2504
+ }
2400
2505
  const outputDir = path2.dirname(outputPath);
2401
2506
  if (!fs2.existsSync(outputDir)) fs2.mkdirSync(outputDir, { recursive: true });
2402
2507
  fs2.writeFileSync(outputPath, newContent);
2508
+ const validation = [];
2509
+ validation.push(validateTokenFileAfterWrite(outputPath, format, tokenCount));
2403
2510
  let rulesResults = [];
2404
2511
  try {
2405
2512
  rulesResults = await syncRulesFiles({
@@ -2408,6 +2515,14 @@ Last updated: ${lastUpdated}`,
2408
2515
  apiBase: apiBase ?? void 0,
2409
2516
  rulesDir: process.cwd()
2410
2517
  });
2518
+ for (const r of rulesResults) {
2519
+ const fullPath = path2.resolve(process.cwd(), r.path);
2520
+ validation.push({
2521
+ path: fullPath,
2522
+ status: r.success && fs2.existsSync(fullPath) ? "OK" : "FAIL",
2523
+ detail: r.success ? "Written." : r.error || "Write failed."
2524
+ });
2525
+ }
2411
2526
  } catch (error) {
2412
2527
  console.error(`[syncAll] Failed to sync rules: ${error}`);
2413
2528
  }
@@ -2428,7 +2543,7 @@ Last updated: ${lastUpdated}`,
2428
2543
  hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
2429
2544
  deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
2430
2545
  });
2431
- return { responseText: response, rulesResults };
2546
+ return { responseText: response, rulesResults, validation };
2432
2547
  }
2433
2548
  switch (name) {
2434
2549
  case "getToken": {
@@ -2525,6 +2640,29 @@ Last updated: ${lastUpdated}`,
2525
2640
  }]
2526
2641
  };
2527
2642
  }
2643
+ case "listTypesets": {
2644
+ const typography = data.tokens.typography;
2645
+ const cssPrefix = args?.cssPrefix || "atmx";
2646
+ const typesets = typography ? buildTypesetsList(typography, cssPrefix) : [];
2647
+ const underlineThickness = typography?.underlineThickness;
2648
+ const underlineOffset = typography?.underlineOffset;
2649
+ return {
2650
+ content: [{
2651
+ type: "text",
2652
+ text: JSON.stringify({
2653
+ count: typesets.length,
2654
+ typesets,
2655
+ instruction: "Emit one CSS rule per typeset using the cssClass and the listed var() properties. Include font-family, font-size, font-weight, line-height; add letter-spacing when present; add text-transform and text-decoration when hasTextTransform/hasTextDecoration are true so the result is 1:1 with the design system.",
2656
+ ...underlineThickness != null && underlineOffset != null && {
2657
+ underlineVars: {
2658
+ thickness: `var(--${cssPrefix}-typography-underline-thickness)`,
2659
+ offset: `var(--${cssPrefix}-typography-underline-offset)`
2660
+ }
2661
+ }
2662
+ }, null, 2)
2663
+ }]
2664
+ };
2665
+ }
2528
2666
  case "validateUsage": {
2529
2667
  const value = args?.value;
2530
2668
  const context = args?.context || "any";
@@ -2796,27 +2934,55 @@ Last updated: ${lastUpdated}`,
2796
2934
  }
2797
2935
  case "syncAll": {
2798
2936
  const skipTokens = args?.skipTokens === true;
2937
+ const dryRun = args?.dryRun === true;
2799
2938
  const output = args?.output || "./tokens.css";
2800
2939
  const format = args?.format || "css";
2801
- const parts = ["\u2713 syncAll complete."];
2940
+ const parts = [dryRun ? "[DRY RUN] syncAll report (no files written)." : "\u2713 syncAll complete."];
2802
2941
  let tokenResponseText = "";
2942
+ const allValidation = [];
2803
2943
  if (!skipTokens) {
2804
- const { responseText, rulesResults } = await performTokenSyncAndRules(data, output, format);
2805
- tokenResponseText = responseText;
2806
- parts.push(`Tokens: ${output} (${format})`);
2807
- if (rulesResults.length > 0) {
2808
- parts.push(`Rules: ${rulesResults.map((r) => r.path).join(", ")}`);
2944
+ const result = await performTokenSyncAndRules(data, output, format, dryRun);
2945
+ tokenResponseText = result.responseText;
2946
+ allValidation.push(...result.validation);
2947
+ if (!dryRun && result.rulesResults.length > 0) {
2948
+ parts.push(`Rules: ${result.rulesResults.map((r) => r.path).join(", ")}`);
2949
+ }
2950
+ if (dryRun) {
2951
+ parts.push(`Would write tokens: ${output} (${format})`);
2952
+ } else {
2953
+ parts.push(`Tokens: ${output} (${format})`);
2809
2954
  }
2810
2955
  }
2811
- const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
2812
- if (!fs2.existsSync(skillsDir)) fs2.mkdirSync(skillsDir, { recursive: true });
2813
2956
  const dsVersion = String(data.meta.version ?? "1.0.0");
2814
2957
  const dsExportedAt = data.meta.exportedAt;
2958
+ const skillsDir = path2.resolve(process.cwd(), ".cursor/skills/atomix-ds");
2959
+ const skillPath1 = path2.join(skillsDir, "SKILL.md");
2960
+ const skillPath2 = path2.join(skillsDir, "FIGMA-SKILL.md");
2961
+ const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
2962
+ if (dryRun) {
2963
+ parts.push(
2964
+ cachedMcpTier === "pro" ? "Would write skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/FIGMA-SKILL.md" : "Would write skills: .cursor/skills/atomix-ds/SKILL.md"
2965
+ );
2966
+ parts.push("Would write manifest: atomix-dependencies.json");
2967
+ const reportText = [parts.join("\n"), tokenResponseText].filter(Boolean).join("\n\n---\n\n");
2968
+ return {
2969
+ content: [{ type: "text", text: `syncAllResult: DRY_RUN (no files written)
2970
+
2971
+ ${reportText}` }]
2972
+ };
2973
+ }
2974
+ if (!fs2.existsSync(skillsDir)) fs2.mkdirSync(skillsDir, { recursive: true });
2815
2975
  const genericWithVersion = injectSkillVersion(GENERIC_SKILL_MD, dsVersion, dsExportedAt);
2816
- const figmaWithVersion = injectSkillVersion(FIGMA_DESIGN_SKILL_MD, dsVersion, dsExportedAt);
2817
- fs2.writeFileSync(path2.join(skillsDir, "SKILL.md"), genericWithVersion);
2818
- fs2.writeFileSync(path2.join(skillsDir, "design-in-figma.md"), figmaWithVersion);
2819
- parts.push("Skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/design-in-figma.md (synced at DS v" + dsVersion + ")");
2976
+ fs2.writeFileSync(skillPath1, genericWithVersion);
2977
+ allValidation.push({ path: skillPath1, status: fs2.existsSync(skillPath1) ? "OK" : "FAIL", detail: "Written." });
2978
+ if (cachedMcpTier === "pro") {
2979
+ const figmaWithVersion = injectSkillVersion(FIGMA_DESIGN_SKILL_MD, dsVersion, dsExportedAt);
2980
+ fs2.writeFileSync(skillPath2, figmaWithVersion);
2981
+ allValidation.push({ path: skillPath2, status: fs2.existsSync(skillPath2) ? "OK" : "FAIL", detail: "Written." });
2982
+ }
2983
+ parts.push(
2984
+ cachedMcpTier === "pro" ? "Skills: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/FIGMA-SKILL.md (synced at DS v" + dsVersion + ")" : "Skills: .cursor/skills/atomix-ds/SKILL.md (synced at DS v" + dsVersion + ")"
2985
+ );
2820
2986
  const tokens = data.tokens;
2821
2987
  const typography = tokens?.typography;
2822
2988
  const fontFamily = typography?.fontFamily;
@@ -2847,19 +3013,22 @@ Last updated: ${lastUpdated}`,
2847
3013
  fonts: { families: fontNames },
2848
3014
  skills: {
2849
3015
  skill: ".cursor/skills/atomix-ds/SKILL.md",
2850
- skillFigmaDesign: ".cursor/skills/atomix-ds/design-in-figma.md",
3016
+ ...cachedMcpTier === "pro" ? { skillFigmaDesign: ".cursor/skills/atomix-ds/FIGMA-SKILL.md" } : {},
2851
3017
  syncedAtVersion: data.meta.version ?? "1.0.0"
2852
3018
  }
2853
3019
  };
2854
- const manifestPath = path2.resolve(process.cwd(), "atomix-dependencies.json");
2855
3020
  fs2.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
3021
+ allValidation.push({ path: manifestPath, status: fs2.existsSync(manifestPath) ? "OK" : "FAIL", detail: "Written." });
2856
3022
  parts.push("Manifest: atomix-dependencies.json (icons, fonts, skill paths)");
2857
3023
  const summary = parts.join("\n");
2858
- const fullText = tokenResponseText ? `${summary}
3024
+ const validationBlock = formatValidationBlock(allValidation);
3025
+ const hasFailure = allValidation.some((e) => e.status === "FAIL");
3026
+ const resultLine = hasFailure ? "syncAllResult: FAIL \u2014 Check VALIDATION section below. Do not report success to the user.\n\n" : "syncAllResult: OK\n\n";
3027
+ const fullText = resultLine + (tokenResponseText ? `${summary}
2859
3028
 
2860
3029
  ---
2861
3030
 
2862
- ${tokenResponseText}` : summary;
3031
+ ${tokenResponseText}${validationBlock}` : `${summary}${validationBlock}`);
2863
3032
  return {
2864
3033
  content: [{ type: "text", text: fullText }]
2865
3034
  };
@@ -2895,16 +3064,18 @@ ${tokenResponseText}` : summary;
2895
3064
  },
2896
3065
  fonts: {
2897
3066
  families: fontNames,
2898
- performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a typeset: CSS rules (e.g. .typeset-display, .typeset-heading, .typeset-body) that use var(--atmx-typography-*) for font-family, font-size, font-weight, line-height, letter-spacing from getToken/listTokens(typography). Do not create a file that only contains a font import."
3067
+ performanceHint: "Link fonts via URL (e.g. Google Fonts <link> or CSS @import); no need to download font files or add them to the repo. Prefer font-display: swap when possible. You must also build a complete typeset CSS: call listTypesets to get every typeset from the design system, then emit one CSS class per typeset (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too so the result is 1:1 with the DS. Use the CSS variable names returned by listTypesets. Do not create a file that only contains a font import."
2899
3068
  },
2900
3069
  skill: {
2901
3070
  path: ".cursor/skills/atomix-ds/SKILL.md",
2902
3071
  content: GENERIC_SKILL_MD
2903
3072
  },
2904
- skillFigmaDesign: {
2905
- path: ".cursor/skills/atomix-ds/design-in-figma.md",
2906
- content: FIGMA_DESIGN_SKILL_MD
2907
- },
3073
+ ...cachedMcpTier === "pro" ? {
3074
+ skillFigmaDesign: {
3075
+ path: ".cursor/skills/atomix-ds/FIGMA-SKILL.md",
3076
+ content: FIGMA_DESIGN_SKILL_MD
3077
+ }
3078
+ } : {},
2908
3079
  tokenFiles: {
2909
3080
  files: ["tokens.css", "tokens.json"],
2910
3081
  copyInstructions: "Call the syncAll MCP tool to create the token file, skills, and atomix-dependencies.json; do not only suggest the user run sync later."
@@ -2971,13 +3142,14 @@ ${tokenResponseText}` : summary;
2971
3142
  if (payloads.numberVariableCollections.length > 0) {
2972
3143
  const numberResults = [];
2973
3144
  try {
3145
+ const foundationsName = payloads.colorVariables.collectionName;
2974
3146
  for (const coll of payloads.numberVariableCollections) {
2975
3147
  const result = await sendBridgeRequest("create_number_variables", {
2976
- collectionName: coll.collectionName,
2977
- variables: coll.variables,
3148
+ collectionName: foundationsName,
3149
+ variables: coll.variables.map((v) => ({ name: `${coll.categoryKey} / ${v.name}`, value: v.value })),
2978
3150
  scopes: coll.scopes
2979
3151
  });
2980
- numberResults.push({ collectionName: coll.collectionName, result });
3152
+ numberResults.push({ categoryKey: coll.categoryKey, result });
2981
3153
  }
2982
3154
  out.numberVariables = numberResults;
2983
3155
  } catch (e) {
@@ -3302,7 +3474,7 @@ ${tokenResponseText}` : summary;
3302
3474
  type: "text",
3303
3475
  text: JSON.stringify({
3304
3476
  error: `Unknown tool: ${name}`,
3305
- availableTools: ["getToken", "listTokens", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma", "getFigmaVariablesAndStyles", "createDesignPlaceholder", "resolveFigmaIdsForTokens", "designCreateFrame", "designCreateText", "designCreateRectangle", "designSetAutoLayout", "designSetLayoutConstraints", "designAppendChild", "getDesignScreenshot", "finalizeDesignFrame"]
3477
+ availableTools: ["getToken", "listTokens", "listTypesets", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncAll", "getDependencies", "syncToFigma", "getFigmaVariablesAndStyles", "createDesignPlaceholder", "resolveFigmaIdsForTokens", "designCreateFrame", "designCreateText", "designCreateRectangle", "designSetAutoLayout", "designSetLayoutConstraints", "designAppendChild", "getDesignScreenshot", "finalizeDesignFrame"]
3306
3478
  }, null, 2)
3307
3479
  }]
3308
3480
  };
@@ -3398,7 +3570,7 @@ Use the returned rules and token paths/values when generating or editing code. P
3398
3570
 
3399
3571
  - **Fetch first:** Before writing UI or styles, call getAIToolRules and/or getToken/listTokens so you know the exact tokens and conventions.
3400
3572
  - **Icons:** Apply the design system's icon tokens when rendering icons: sizing via \`getToken("sizing.icon.sm")\` or \`listTokens("sizing")\`, and stroke width via \`getToken("icons.strokeWidth")\` when the DS defines it; do not use hardcoded sizes or stroke widths.
3401
- - **Typography:** Use typography tokens (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) from the DS for any text; build typesets (Display, Heading, body) from those tokens when creating global styles.
3573
+ - **Typography:** Use typography tokens from the DS for any text. When creating global typeset CSS, call **listTypesets** and emit one CSS class per typeset (do not skip any); include text-transform and text-decoration when present for 1:1 match.
3402
3574
  - **No guessing:** If a value is not in the rules or token list, use searchTokens or listTokens to find the closest match rather than inventing a value.
3403
3575
  - **Version check:** If this skill file has frontmatter \`atomixDsVersion\`, compare it to the design system version from **getDependencies** (\`meta.designSystemVersion\`). If the design system is newer, suggest the user run **syncAll** to update skills and tokens.
3404
3576
  `;
@@ -3463,18 +3635,15 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3463
3635
  min-height: 100vh;
3464
3636
  padding: 2rem;
3465
3637
  display: flex;
3466
- align-items: center;
3467
- justify-content: center;
3468
- text-align: center;
3469
3638
  }
3470
3639
  .wrap { width: 375px; max-width: 100%; }
3471
3640
  .icon { width: 2rem; height: 2rem; margin: 0 auto 1rem; }
3472
3641
  h1 {
3473
3642
  font-family: {{HEADING_FONT_VAR}}, {{FONT_FAMILY_VAR}}, system-ui, sans-serif;
3474
- font-size: clamp(1.5rem, 4vw, 2.25rem);
3643
+ font-size: clamp(3rem, 4vw, 5rem);
3475
3644
  font-weight: 700;
3476
3645
  margin: 0 0 0.75rem;
3477
- line-height: 1.25;
3646
+ line-height: 1.2;
3478
3647
  }
3479
3648
  .lead { margin: 0 0 1.5rem; font-size: 1rem; line-height: 1.5; opacity: 0.95; }
3480
3649
  .now { margin: 1.5rem 0 0; font-size: 0.875rem; line-height: 1.6; opacity: 0.95; text-align: left; }
@@ -3487,10 +3656,10 @@ var SHOWCASE_HTML_TEMPLATE = `<!DOCTYPE html>
3487
3656
  <body>
3488
3657
  <div class="wrap">
3489
3658
  <div class="icon" aria-hidden="true">
3490
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="32" height="32"><path d="M20 6L9 17l-5-5"/></svg>
3659
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="44" height="44"><path d="M20 6L9 17l-5-5"/></svg>
3491
3660
  </div>
3492
3661
  <h1>You're all set with {{DS_NAME}}</h1>
3493
- <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon. Token file is linked above.</p>
3662
+ <p class="lead">This page uses your design system: brand primary as background, headline and body typesets, and an icon.</p>
3494
3663
  <div class="now">
3495
3664
  <strong>What you can do now:</strong>
3496
3665
  <ul>
@@ -4123,8 +4292,8 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4123
4292
 
4124
4293
  - Resolve platform/stack: infer from the project (e.g. package.json, build.gradle, Xcode) or ask once: "Which platform? (e.g. web, Android, iOS)" and if relevant "Which stack? (e.g. React, Vue, Next, Swift, Kotlin)." Do not assume a default.
4125
4294
  - Call **getDependencies** with \`platform\` and optional \`stack\`. If it fails, tell the user the design system could not be reached and stop.
4126
- - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, .cursor/skills/atomix-ds/design-in-figma.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4127
- - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md), **Figma design skill** (.cursor/skills/atomix-ds/design-in-figma.md), token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4295
+ - Scan the repo for: .cursor/skills/atomix-ds/SKILL.md, a tokens file (e.g. tokens.css or src/tokens.css), icon package from getDependencies, font links. If getDependencies returned \`skillFigmaDesign\`, also scan for .cursor/skills/atomix-ds/FIGMA-SKILL.md. **Web:** note any existing CSS (globals.css, main.css, Tailwind, etc.). **Native:** note any theme/style files (SwiftUI, Android themes, Compose).
4296
+ - Build two lists: **Suggested** (from getDependencies minus what exists) and **Already present**. Include: icon package, font links, skill (.cursor/skills/atomix-ds/SKILL.md). Include **Figma design skill** (.cursor/skills/atomix-ds/FIGMA-SKILL.md) in Suggested/Already present only if getDependencies returned a \`skillFigmaDesign\` object; otherwise omit it. Include token files; for web, also include the **showcase page** (atomix-setup-showcase.html) if getDependencies returned a \`showcase\` object.
4128
4297
  - Do not write, create, or add anything in Phase 1.
4129
4298
 
4130
4299
  ## Phase 2 \u2013 Report and ask
@@ -4138,14 +4307,14 @@ Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/bord
4138
4307
  - Run only when the user has said yes (all or specific items).
4139
4308
  - For each approved item:
4140
4309
  - **Skill:** Write the skill content from getDependencies \`skill.content\` to \`skill.path\` (.cursor/skills/atomix-ds/SKILL.md).
4141
- - **Figma design skill:** Write the skill content from getDependencies \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` (.cursor/skills/atomix-ds/design-in-figma.md). Use this when designing in Figma so the agent follows principal-product-designer rules and prefers existing Figma variables.
4310
+ - **Figma design skill:** Only if getDependencies returned \`skillFigmaDesign\`, write \`skillFigmaDesign.content\` to \`skillFigmaDesign.path\` (.cursor/skills/atomix-ds/FIGMA-SKILL.md). Use this when designing in Figma so the agent follows principal-product-designer rules and prefers existing Figma variables. If \`skillFigmaDesign\` was not in the response, do not add this file.
4142
4311
  - **Token file:** Call **syncAll** with \`output\` set to the path (e.g. "./src/tokens.css" or "./tokens.css"). syncAll also writes skills and atomix-dependencies.json. You must call syncAll; do not only suggest the user run it later.
4143
4312
  - **Icon package:** Install per getDependencies. When rendering icons, apply the design system's icon tokens: use getToken(\`sizing.icon.*\`) or listTokens(\`sizing\`) for size, and getToken(\`icons.strokeWidth\`) for stroke width when the DS defines it; do not use hardcoded sizes or stroke widths.
4144
- - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **typeset** in CSS: use **getToken** / **listTokens** (category \`typography\`) to get fontFamily, fontSize, fontWeight, lineHeight, letterSpacing for display, heading, and body, and write CSS rules (e.g. \`.typeset-display\`, \`.typeset-heading\`, \`.typeset-body\`, or \`h1\`/\`h2\`/\`p\`) that set those properties to \`var(--atmx-typography-*)\`. The typeset file (or section) must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4313
+ - **Fonts and typeset:** Add font links (e.g. \`<link>\` or \`@import\` from Google Fonts). Then build a **complete typeset CSS**: call **listTypesets** to get every typeset from the owner's design system (do not skip any). Emit **one CSS rule per typeset** using the \`cssClass\` and the \`fontFamilyVar\`, \`fontSizeVar\`, \`fontWeightVar\`, \`lineHeightVar\` (and \`letterSpacingVar\`, \`textTransformVar\`, \`textDecorationVar\` when present) returned by listTypesets. Include text-transform and text-decoration when the typeset has them so the result is **1:1** with the design system. The typeset file must define the full type scale\u2014not only a font import. Do not create a CSS file that contains only a font import.
4145
4314
  - **Showcase page (web only):** If platform is web and getDependencies returned a \`showcase\` object, create the file at \`showcase.path\` using \`showcase.template\`. Replace every placeholder per \`showcase.substitutionInstructions\`: TOKENS_CSS_PATH, DS_NAME, BRAND_PRIMARY_VAR (page background), BRAND_PRIMARY_FOREGROUND_VAR (text on brand), HEADING_FONT_VAR (h1), FONT_FAMILY_VAR (body), FONT_LINK_TAG. Use only CSS variable names that exist in the synced token file. Do not change the HTML structure. After creating the file, launch it in the default browser (e.g. \`open atomix-setup-showcase.html\` on macOS, \`xdg-open atomix-setup-showcase.html\` on Linux, or the equivalent on Windows).
4146
4315
  - Report only what you actually created or updated. Do not claim the token file was added if you did not call syncAll.
4147
4316
  - **After reporting \u2013 styles/theme:**
4148
- - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014CSS classes or element rules that set font-family, font-size, font-weight, line-height, letter-spacing using \`var(--atmx-typography-*)\` from the token file (e.g. \`.typeset-display\`, \`.typeset-heading\`, \`.typeset-body\`). You must call getToken/listTokens to get the exact typography token paths and write the corresponding var() references. The output must not be only a font import; it must define the full typeset (Display, Heading, body) with every style detail from the design system.
4317
+ - **Web:** If the project already has at least one CSS file: recommend how to integrate Atomix (e.g. import the synced tokens file, use \`var(--atmx-*)\`). Do not suggest a new global CSS. Only if there is **no** CSS file at all, ask once: "There are no CSS files yet. Do you want me to build a global typeset from the design system?" If yes, create a CSS file that includes: (1) font \`@import\` or document that a font link is needed, and (2) **typeset rules**\u2014call **listTypesets** and emit **one CSS class per typeset** (do not skip any). For each class set font-family, font-size, font-weight, line-height, letter-spacing; when the typeset has text-transform or text-decoration, set those too for a 1:1 match. Use the CSS variable names returned by listTypesets. The output must not be only a font import; it must define every typeset with every style detail from the design system.
4149
4318
  - **iOS/Android:** If the project already has theme/style files: recommend how to integrate Atomix tokens. Do not suggest a new global theme. Only if there is **no** theme/style at all, ask once: "There's no theme/style setup yet. Do you want a minimal token-based theme?" and add only if the user says yes.
4150
4319
 
4151
4320
  Create your todo list first, then Phase 1 (resolve platform/stack, call getDependencies, scan, build lists), then Phase 2 (report and ask). Do not perform Phase 3 until the user replies.`;