@atomixstudio/mcp 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/dist/index.js +970 -1168
- package/dist/index.js.map +1 -1
- package/package.json +1 -49
package/dist/index.js
CHANGED
|
@@ -11,27 +11,34 @@ import {
|
|
|
11
11
|
ListPromptsRequestSchema,
|
|
12
12
|
GetPromptRequestSchema
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
fetchDesignSystem,
|
|
16
|
+
getTokenByPath,
|
|
17
|
+
flattenTokens,
|
|
18
|
+
searchTokens,
|
|
19
|
+
getTokenStats,
|
|
20
|
+
compareDesignSystems,
|
|
21
|
+
generateCSSOutput,
|
|
22
|
+
generateSCSSOutput,
|
|
23
|
+
generateLessOutput,
|
|
24
|
+
generateJSONOutput,
|
|
25
|
+
generateTSOutput,
|
|
26
|
+
generateJSOutput,
|
|
27
|
+
generateSwiftOutput,
|
|
28
|
+
generateKotlinOutput,
|
|
29
|
+
generateDartOutput,
|
|
30
|
+
diffTokens,
|
|
31
|
+
formatSyncResponse,
|
|
32
|
+
detectGovernanceChangesByFoundation,
|
|
33
|
+
syncRulesFiles
|
|
34
|
+
} from "@atomixstudio/sync-core";
|
|
15
35
|
import * as path from "path";
|
|
16
|
-
import * as
|
|
36
|
+
import * as fs from "fs";
|
|
17
37
|
function parseArgs() {
|
|
18
38
|
const args = process.argv.slice(2);
|
|
19
|
-
let command = "server";
|
|
20
39
|
let dsId2 = null;
|
|
21
40
|
let apiKey2 = null;
|
|
22
41
|
let apiBase2 = null;
|
|
23
|
-
let output = null;
|
|
24
|
-
let format = null;
|
|
25
|
-
let exclude = [];
|
|
26
|
-
let yes = false;
|
|
27
|
-
let rules = null;
|
|
28
|
-
let rulesDir = null;
|
|
29
|
-
if (args[0] && !args[0].startsWith("-")) {
|
|
30
|
-
const cmd = args[0].toLowerCase();
|
|
31
|
-
if (cmd === "sync") command = "sync";
|
|
32
|
-
else if (cmd === "init") command = "init";
|
|
33
|
-
else if (cmd === "help" || cmd === "--help" || cmd === "-h") command = "help";
|
|
34
|
-
}
|
|
35
42
|
for (let i = 0; i < args.length; i++) {
|
|
36
43
|
if (args[i] === "--ds-id" && args[i + 1]) {
|
|
37
44
|
dsId2 = args[i + 1];
|
|
@@ -42,125 +49,51 @@ function parseArgs() {
|
|
|
42
49
|
} else if (args[i] === "--api-base" && args[i + 1]) {
|
|
43
50
|
apiBase2 = args[i + 1];
|
|
44
51
|
i++;
|
|
45
|
-
} else if ((args[i] === "--output" || args[i] === "-o") && args[i + 1]) {
|
|
46
|
-
output = args[i + 1];
|
|
47
|
-
i++;
|
|
48
|
-
} else if (args[i] === "--format" && args[i + 1]) {
|
|
49
|
-
const f = args[i + 1].toLowerCase();
|
|
50
|
-
const validFormats = ["css", "scss", "less", "json", "ts", "js", "swift", "kotlin", "dart"];
|
|
51
|
-
if (validFormats.includes(f)) format = f;
|
|
52
|
-
i++;
|
|
53
|
-
} else if (args[i] === "--exclude" && args[i + 1]) {
|
|
54
|
-
exclude.push(args[i + 1]);
|
|
55
|
-
i++;
|
|
56
|
-
} else if (args[i] === "-y" || args[i] === "--yes") {
|
|
57
|
-
yes = true;
|
|
58
|
-
} else if (args[i] === "--rules" || args[i] === "-r") {
|
|
59
|
-
rules = true;
|
|
60
|
-
} else if (args[i] === "--no-rules") {
|
|
61
|
-
rules = false;
|
|
62
|
-
} else if (args[i] === "--rules-dir" && args[i + 1]) {
|
|
63
|
-
rulesDir = args[i + 1];
|
|
64
|
-
rules = true;
|
|
65
|
-
i++;
|
|
66
52
|
}
|
|
67
53
|
}
|
|
68
|
-
|
|
69
|
-
return { command, dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2, output, format, exclude, yes, rules: effectiveRules, rulesDir };
|
|
54
|
+
return { dsId: dsId2, apiKey: apiKey2, apiBase: apiBase2 };
|
|
70
55
|
}
|
|
71
56
|
var cliArgs = parseArgs();
|
|
72
57
|
var { dsId, apiKey } = cliArgs;
|
|
73
58
|
var apiBase = cliArgs.apiBase || "https://atomixstudio.eu";
|
|
74
59
|
var cachedData = null;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const url = `${apiBase}/api/ds/${dsId}/tokens?format=export`;
|
|
81
|
-
const headers = {
|
|
82
|
-
"Content-Type": "application/json"
|
|
83
|
-
};
|
|
84
|
-
if (apiKey) {
|
|
85
|
-
headers["x-api-key"] = apiKey;
|
|
86
|
-
}
|
|
87
|
-
const response = await fetch(url, { headers });
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
const text = await response.text();
|
|
90
|
-
throw new Error(`Failed to fetch design system: ${response.status} ${text}`);
|
|
91
|
-
}
|
|
92
|
-
const data = await response.json();
|
|
93
|
-
cachedData = data;
|
|
94
|
-
return data;
|
|
95
|
-
}
|
|
96
|
-
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
97
|
-
function getTokenByPath(tokens, path2) {
|
|
98
|
-
const parts = path2.split(".");
|
|
99
|
-
let current = tokens;
|
|
100
|
-
for (const part of parts) {
|
|
101
|
-
if (current && typeof current === "object" && part in current) {
|
|
102
|
-
current = current[part];
|
|
103
|
-
} else {
|
|
104
|
-
return void 0;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return current;
|
|
60
|
+
var cachedETag = null;
|
|
61
|
+
var lastChangeSummary = null;
|
|
62
|
+
var lastSyncAffectedTokens = null;
|
|
63
|
+
function getLastChangeSummary() {
|
|
64
|
+
return lastChangeSummary;
|
|
108
65
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else {
|
|
117
|
-
results.push({ path: newPath, value });
|
|
118
|
-
}
|
|
66
|
+
async function updateChangeSummary(freshData) {
|
|
67
|
+
if (cachedData) {
|
|
68
|
+
const changes = compareDesignSystems(cachedData, freshData);
|
|
69
|
+
lastChangeSummary = changes.summary;
|
|
70
|
+
if (changes.hasChanges) {
|
|
71
|
+
console.error(`[mcp-user] Design system changes detected:
|
|
72
|
+
${changes.summary}`);
|
|
119
73
|
}
|
|
120
74
|
}
|
|
121
|
-
return results;
|
|
122
75
|
}
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
76
|
+
async function fetchDesignSystemForMCP(forceRefresh = false) {
|
|
77
|
+
if (!dsId) throw new Error("Missing --ds-id. Usage: npx @atomixstudio/mcp --ds-id <id>");
|
|
78
|
+
const result = await fetchDesignSystem({
|
|
79
|
+
dsId,
|
|
80
|
+
apiKey: apiKey ?? void 0,
|
|
81
|
+
apiBase: apiBase ?? void 0,
|
|
82
|
+
etag: forceRefresh ? void 0 : cachedETag ?? void 0,
|
|
83
|
+
forceRefresh
|
|
130
84
|
});
|
|
85
|
+
if (result.status === 304 && cachedData) return cachedData;
|
|
86
|
+
if (result.status === 304 || !result.data) throw new Error("No design system data (304 or null)");
|
|
87
|
+
cachedData = result.data;
|
|
88
|
+
cachedETag = result.etag;
|
|
89
|
+
await updateChangeSummary(result.data);
|
|
90
|
+
return result.data;
|
|
131
91
|
}
|
|
132
|
-
|
|
133
|
-
let count = 0;
|
|
134
|
-
for (const [key, value] of Object.entries(tokens)) {
|
|
135
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
136
|
-
count += countTokens(value, `${prefix}${key}.`);
|
|
137
|
-
} else if (value !== void 0 && value !== null) {
|
|
138
|
-
count++;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return count;
|
|
142
|
-
}
|
|
143
|
-
function getTokenStats(data) {
|
|
144
|
-
const byCategory = {};
|
|
145
|
-
let total = 0;
|
|
146
|
-
for (const [category, value] of Object.entries(data.tokens)) {
|
|
147
|
-
if (value && typeof value === "object") {
|
|
148
|
-
const count = countTokens(value);
|
|
149
|
-
byCategory[category] = count;
|
|
150
|
-
total += count;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return {
|
|
154
|
-
total,
|
|
155
|
-
byCategory,
|
|
156
|
-
cssVariables: Object.keys(data.cssVariables).length,
|
|
157
|
-
governanceRules: data.governance.rules.length
|
|
158
|
-
};
|
|
159
|
-
}
|
|
92
|
+
var TOKEN_CATEGORIES = ["colors", "typography", "spacing", "sizing", "shadows", "radius", "borders", "motion", "zIndex"];
|
|
160
93
|
var server = new Server(
|
|
161
94
|
{
|
|
162
95
|
name: "atomix-mcp-user",
|
|
163
|
-
version: "1.0.
|
|
96
|
+
version: "1.0.9"
|
|
164
97
|
},
|
|
165
98
|
{
|
|
166
99
|
capabilities: {
|
|
@@ -286,7 +219,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
286
219
|
},
|
|
287
220
|
{
|
|
288
221
|
name: "syncTokens",
|
|
289
|
-
description: "Sync design tokens to a local file.
|
|
222
|
+
description: "Sync design tokens to a local file. Safe, never breaks UI - adds new tokens, updates existing values, marks deprecated tokens. Use /refactor to migrate deprecated tokens. WARNING: The output file is completely rewritten - only CSS custom properties (variables) are preserved. Custom CSS rules will be lost. Keep custom CSS in a separate file.",
|
|
290
223
|
inputSchema: {
|
|
291
224
|
type: "object",
|
|
292
225
|
properties: {
|
|
@@ -302,6 +235,25 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
302
235
|
},
|
|
303
236
|
required: ["output"]
|
|
304
237
|
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "getDependencies",
|
|
241
|
+
description: "Get suggested dependencies for this design system (icon package, fonts, SKILL.md, MCP config, token files). Use with atomix/install. Optional platform and stack for tailored suggestions.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
platform: {
|
|
246
|
+
type: "string",
|
|
247
|
+
enum: ["web", "ios", "android"],
|
|
248
|
+
description: "Target platform (web, ios, android). Optional."
|
|
249
|
+
},
|
|
250
|
+
stack: {
|
|
251
|
+
type: "string",
|
|
252
|
+
description: "Stack or framework (e.g. react, vue, next, swift, kotlin). Optional."
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
required: []
|
|
256
|
+
}
|
|
305
257
|
}
|
|
306
258
|
]
|
|
307
259
|
};
|
|
@@ -309,7 +261,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
309
261
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
310
262
|
const { name, arguments: args } = request.params;
|
|
311
263
|
try {
|
|
312
|
-
const
|
|
264
|
+
const shouldForceRefresh = name === "syncTokens";
|
|
265
|
+
const data = await fetchDesignSystemForMCP(shouldForceRefresh);
|
|
313
266
|
switch (name) {
|
|
314
267
|
case "getToken": {
|
|
315
268
|
const path2 = args?.path;
|
|
@@ -359,16 +312,33 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
359
312
|
};
|
|
360
313
|
}
|
|
361
314
|
const flat = flattenTokens(tokensToList);
|
|
315
|
+
const tokensWithCssVars = flat.map(({ path: path2, value }) => {
|
|
316
|
+
const fullPath = subcategory ? `${category}.${subcategory}.${path2}` : `${category}.${path2}`;
|
|
317
|
+
let cssVar;
|
|
318
|
+
if (category === "colors" && subcategory === "static.brand") {
|
|
319
|
+
cssVar = data.cssVariables[`--atmx-color-brand-${path2}`];
|
|
320
|
+
} else if (category === "colors" && subcategory?.startsWith("modes.")) {
|
|
321
|
+
cssVar = data.cssVariables[`--atmx-color-${path2}`];
|
|
322
|
+
} else {
|
|
323
|
+
const cssVarKey = `--atmx-${fullPath.replace(/\./g, "-")}`;
|
|
324
|
+
cssVar = data.cssVariables[cssVarKey];
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
path: fullPath,
|
|
328
|
+
value,
|
|
329
|
+
cssVariable: cssVar || void 0
|
|
330
|
+
};
|
|
331
|
+
});
|
|
362
332
|
return {
|
|
363
333
|
content: [{
|
|
364
334
|
type: "text",
|
|
365
335
|
text: JSON.stringify({
|
|
366
336
|
category,
|
|
367
337
|
subcategory,
|
|
368
|
-
count:
|
|
369
|
-
tokens:
|
|
338
|
+
count: tokensWithCssVars.length,
|
|
339
|
+
tokens: tokensWithCssVars.slice(0, 50),
|
|
370
340
|
// Limit to 50 for readability
|
|
371
|
-
truncated:
|
|
341
|
+
truncated: tokensWithCssVars.length > 50
|
|
372
342
|
}, null, 2)
|
|
373
343
|
}]
|
|
374
344
|
};
|
|
@@ -529,43 +499,114 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
529
499
|
1. Create \`.cursor/mcp.json\` in your project root
|
|
530
500
|
2. Add the MCP configuration (use exportMCPConfig to get it)
|
|
531
501
|
3. Restart Cursor IDE
|
|
532
|
-
4. Verify by asking: "What design tokens are available?"
|
|
533
|
-
|
|
502
|
+
4. Verify by asking: "What design tokens are available?"
|
|
503
|
+
|
|
504
|
+
## File Structure
|
|
505
|
+
|
|
506
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
534
507
|
|
|
535
|
-
|
|
508
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
509
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
510
|
+
|
|
511
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
512
|
+
copilot: `# Copilot Setup
|
|
513
|
+
|
|
514
|
+
1. Create a Copilot instructions file in your project (e.g. \`.github/copilot-instructions.md\`)
|
|
536
515
|
2. Use getAIToolRules({ tool: "copilot" }) to get the content
|
|
537
|
-
3. Enable custom instructions in
|
|
538
|
-
|
|
516
|
+
3. Enable custom instructions in your editor (e.g. \`github.copilot.chat.codeGeneration.useInstructionFiles\`: true in settings)
|
|
517
|
+
|
|
518
|
+
## File Structure
|
|
519
|
+
|
|
520
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
521
|
+
|
|
522
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
523
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
524
|
+
|
|
525
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
539
526
|
windsurf: `# Windsurf Setup
|
|
540
527
|
|
|
541
528
|
1. Create \`.windsurf/mcp.json\` in your project root
|
|
542
529
|
2. Create \`.windsurfrules\` with rules from getAIToolRules
|
|
543
|
-
3. Restart Windsurf Editor
|
|
530
|
+
3. Restart Windsurf Editor
|
|
531
|
+
|
|
532
|
+
## File Structure
|
|
533
|
+
|
|
534
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
535
|
+
|
|
536
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
537
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
538
|
+
|
|
539
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
544
540
|
cline: `# Cline Setup
|
|
545
541
|
|
|
546
542
|
1. Create \`.clinerules\` in your project root
|
|
547
|
-
2. Cline auto-detects MCP from .cursor/mcp.json
|
|
543
|
+
2. Cline auto-detects MCP from .cursor/mcp.json
|
|
544
|
+
|
|
545
|
+
## File Structure
|
|
546
|
+
|
|
547
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
548
|
+
|
|
549
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
550
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
551
|
+
|
|
552
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
548
553
|
continue: `# Continue Setup
|
|
549
554
|
|
|
550
555
|
1. Create/edit \`.continue/config.json\`
|
|
551
556
|
2. Add mcpServers configuration
|
|
552
|
-
3. Restart VS Code
|
|
557
|
+
3. Restart VS Code
|
|
558
|
+
|
|
559
|
+
## File Structure
|
|
560
|
+
|
|
561
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
562
|
+
|
|
563
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
564
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
565
|
+
|
|
566
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
553
567
|
zed: `# Zed Setup
|
|
554
568
|
|
|
555
569
|
1. Create \`.zed/assistant/rules.md\` in your project
|
|
556
|
-
2. Use getAIToolRules({ tool: "zed" }) for content
|
|
570
|
+
2. Use getAIToolRules({ tool: "zed" }) for content
|
|
571
|
+
|
|
572
|
+
## File Structure
|
|
573
|
+
|
|
574
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
575
|
+
|
|
576
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
577
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
578
|
+
|
|
579
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
557
580
|
"claude-desktop": `# Claude Desktop Setup
|
|
558
581
|
|
|
559
582
|
1. Find your Claude config:
|
|
560
583
|
- macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
|
561
584
|
- Windows: %APPDATA%\\Claude\\claude_desktop_config.json
|
|
562
585
|
2. Add MCP server configuration
|
|
563
|
-
3. Restart Claude Desktop
|
|
586
|
+
3. Restart Claude Desktop
|
|
587
|
+
|
|
588
|
+
## File Structure
|
|
589
|
+
|
|
590
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
591
|
+
|
|
592
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
593
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
594
|
+
|
|
595
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`,
|
|
564
596
|
generic: `# Generic AI Tool Setup
|
|
565
597
|
|
|
566
598
|
1. Create AI_GUIDELINES.md in your project root
|
|
567
599
|
2. Use getAIToolRules({ tool: "generic" }) for content
|
|
568
|
-
3. Reference in your prompts or context
|
|
600
|
+
3. Reference in your prompts or context
|
|
601
|
+
|
|
602
|
+
## File Structure
|
|
603
|
+
|
|
604
|
+
\u26A0\uFE0F **IMPORTANT**: The \`tokens.css\` file (or your specified output file) is **completely rewritten** on each sync.
|
|
605
|
+
|
|
606
|
+
- \u2705 **Preserved**: CSS custom properties (variables) - both design system tokens and custom variables
|
|
607
|
+
- \u274C **Lost**: Regular CSS rules (selectors, classes, media queries, etc.)
|
|
608
|
+
|
|
609
|
+
**Best Practice**: Keep \`tokens.css\` separate from your custom CSS. Use a separate file (e.g., \`custom.css\`) for custom styles.`
|
|
569
610
|
};
|
|
570
611
|
const instruction = instructions[tool];
|
|
571
612
|
if (!instruction) {
|
|
@@ -597,17 +638,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
597
638
|
}]
|
|
598
639
|
};
|
|
599
640
|
}
|
|
641
|
+
const outputPath = path.resolve(process.cwd(), output);
|
|
642
|
+
const fileExists = fs.existsSync(outputPath);
|
|
643
|
+
const deprecatedTokens = /* @__PURE__ */ new Map();
|
|
644
|
+
const existingTokens = /* @__PURE__ */ new Map();
|
|
645
|
+
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
646
|
+
const oldContent = fs.readFileSync(outputPath, "utf-8");
|
|
647
|
+
const oldVarPattern = /(?:^|\n)\s*(?:\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\/\s*)?(--[a-zA-Z0-9-]+):\s*([^;]+);/gm;
|
|
648
|
+
let match;
|
|
649
|
+
while ((match = oldVarPattern.exec(oldContent)) !== null) {
|
|
650
|
+
const varName = match[1];
|
|
651
|
+
const varValue = match[2].trim();
|
|
652
|
+
existingTokens.set(varName, varValue);
|
|
653
|
+
if (!(varName in data.cssVariables)) {
|
|
654
|
+
deprecatedTokens.set(varName, varValue);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const mergedCssVariables = { ...data.cssVariables };
|
|
600
659
|
const darkModeColors = data.tokens?.colors?.modes;
|
|
601
660
|
let newContent;
|
|
602
661
|
switch (format) {
|
|
603
662
|
case "css":
|
|
604
|
-
newContent = generateCSSOutput(
|
|
663
|
+
newContent = generateCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
605
664
|
break;
|
|
606
665
|
case "scss":
|
|
607
|
-
newContent = generateSCSSOutput(
|
|
666
|
+
newContent = generateSCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
608
667
|
break;
|
|
609
668
|
case "less":
|
|
610
|
-
newContent = generateLessOutput(
|
|
669
|
+
newContent = generateLessOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
611
670
|
break;
|
|
612
671
|
case "json":
|
|
613
672
|
newContent = generateJSONOutput(data.tokens);
|
|
@@ -619,29 +678,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
619
678
|
newContent = generateTSOutput(data.tokens);
|
|
620
679
|
break;
|
|
621
680
|
case "swift":
|
|
622
|
-
newContent = generateSwiftOutput(
|
|
681
|
+
newContent = generateSwiftOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
623
682
|
break;
|
|
624
683
|
case "kotlin":
|
|
625
|
-
newContent = generateKotlinOutput(
|
|
684
|
+
newContent = generateKotlinOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
626
685
|
break;
|
|
627
686
|
case "dart":
|
|
628
|
-
newContent = generateDartOutput(
|
|
687
|
+
newContent = generateDartOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
629
688
|
break;
|
|
630
689
|
default:
|
|
631
|
-
newContent = generateCSSOutput(
|
|
690
|
+
newContent = generateCSSOutput(mergedCssVariables, darkModeColors?.dark, deprecatedTokens);
|
|
632
691
|
}
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
const
|
|
692
|
+
const tokenCount = Object.keys(mergedCssVariables).length;
|
|
693
|
+
const dsTokenCount = Object.keys(data.cssVariables).length;
|
|
694
|
+
const deprecatedCount = deprecatedTokens.size;
|
|
636
695
|
let diffSummary = "";
|
|
637
696
|
let changes = [];
|
|
697
|
+
let diff;
|
|
638
698
|
if (fileExists && ["css", "scss", "less"].includes(format)) {
|
|
639
699
|
const oldContent = fs.readFileSync(outputPath, "utf-8");
|
|
640
|
-
|
|
641
|
-
const lightChanges = diff.added.length + diff.modified.length
|
|
642
|
-
const darkChanges = diff.addedDark.length + diff.modifiedDark.length
|
|
643
|
-
const totalChanges = lightChanges + darkChanges;
|
|
700
|
+
diff = diffTokens(oldContent, mergedCssVariables, format, darkModeColors?.dark);
|
|
701
|
+
const lightChanges = diff.added.length + diff.modified.length;
|
|
702
|
+
const darkChanges = diff.addedDark.length + diff.modifiedDark.length;
|
|
703
|
+
const totalChanges = lightChanges + darkChanges + deprecatedCount;
|
|
644
704
|
if (totalChanges === 0) {
|
|
705
|
+
const lastUpdated = data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleString() : "N/A";
|
|
645
706
|
return {
|
|
646
707
|
content: [{
|
|
647
708
|
type: "text",
|
|
@@ -649,40 +710,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
649
710
|
|
|
650
711
|
File: ${output}
|
|
651
712
|
Tokens: ${tokenCount}
|
|
652
|
-
Version: ${data.meta.version}
|
|
713
|
+
Version: ${data.meta.version}
|
|
714
|
+
Last updated: ${lastUpdated}`
|
|
653
715
|
}]
|
|
654
716
|
};
|
|
655
717
|
}
|
|
656
718
|
changes = [...diff.modified, ...diff.modifiedDark];
|
|
657
|
-
const lightSummary = lightChanges > 0 ? `Light: ${diff.modified.length} modified, ${diff.added.length} added
|
|
658
|
-
const darkSummary = darkChanges > 0 ? `Dark: ${diff.modifiedDark.length} modified, ${diff.addedDark.length} added
|
|
659
|
-
|
|
719
|
+
const lightSummary = lightChanges > 0 ? `Light: ${diff.modified.length} modified, ${diff.added.length} added` : "";
|
|
720
|
+
const darkSummary = darkChanges > 0 ? `Dark: ${diff.modifiedDark.length} modified, ${diff.addedDark.length} added` : "";
|
|
721
|
+
const deprecatedSummary = deprecatedCount > 0 ? `${deprecatedCount} deprecated (use /refactor)` : "";
|
|
722
|
+
diffSummary = [lightSummary, darkSummary, deprecatedSummary].filter(Boolean).join(" | ");
|
|
723
|
+
const removedTokensWithValues = [];
|
|
724
|
+
for (const [token, value] of deprecatedTokens.entries()) {
|
|
725
|
+
removedTokensWithValues.push({ token, lastValue: value });
|
|
726
|
+
}
|
|
727
|
+
lastSyncAffectedTokens = {
|
|
728
|
+
modified: [...diff.modified, ...diff.modifiedDark].map((m) => ({
|
|
729
|
+
token: m.key,
|
|
730
|
+
oldValue: m.old,
|
|
731
|
+
newValue: m.new
|
|
732
|
+
})),
|
|
733
|
+
removed: removedTokensWithValues,
|
|
734
|
+
added: [...diff.added, ...diff.addedDark],
|
|
735
|
+
format,
|
|
736
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
737
|
+
};
|
|
660
738
|
}
|
|
661
739
|
const outputDir = path.dirname(outputPath);
|
|
662
740
|
if (!fs.existsSync(outputDir)) {
|
|
663
741
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
664
742
|
}
|
|
665
743
|
fs.writeFileSync(outputPath, newContent);
|
|
666
|
-
let
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (changes.length > 0 && changes.length <= 10) {
|
|
677
|
-
response += "\nModified tokens:\n";
|
|
678
|
-
for (const { key, old: oldVal, new: newVal } of changes) {
|
|
679
|
-
response += ` ${key}: ${oldVal} \u2192 ${newVal}
|
|
680
|
-
`;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
} else if (!fileExists) {
|
|
684
|
-
response += "\n(New file created)";
|
|
744
|
+
let rulesResults = [];
|
|
745
|
+
try {
|
|
746
|
+
rulesResults = await syncRulesFiles({
|
|
747
|
+
dsId,
|
|
748
|
+
apiKey: apiKey ?? void 0,
|
|
749
|
+
apiBase: apiBase ?? void 0,
|
|
750
|
+
rulesDir: process.cwd()
|
|
751
|
+
});
|
|
752
|
+
} catch (error) {
|
|
753
|
+
console.error(`[syncTokens] Failed to sync rules: ${error}`);
|
|
685
754
|
}
|
|
755
|
+
const governanceChanges = cachedData ? detectGovernanceChangesByFoundation(cachedData, data) : [];
|
|
756
|
+
const response = formatSyncResponse({
|
|
757
|
+
data,
|
|
758
|
+
output,
|
|
759
|
+
format,
|
|
760
|
+
dsTokenCount,
|
|
761
|
+
deprecatedCount,
|
|
762
|
+
deprecatedTokens,
|
|
763
|
+
diff,
|
|
764
|
+
changes,
|
|
765
|
+
fileExists,
|
|
766
|
+
rulesResults,
|
|
767
|
+
governanceChanges,
|
|
768
|
+
changeSummary: getLastChangeSummary(),
|
|
769
|
+
hasRefactorRecommendation: !!lastSyncAffectedTokens?.removed.length,
|
|
770
|
+
deprecatedTokenCount: lastSyncAffectedTokens?.removed.length || 0
|
|
771
|
+
});
|
|
686
772
|
return {
|
|
687
773
|
content: [{
|
|
688
774
|
type: "text",
|
|
@@ -690,55 +776,210 @@ ${diffSummary}
|
|
|
690
776
|
}]
|
|
691
777
|
};
|
|
692
778
|
}
|
|
779
|
+
case "getDependencies": {
|
|
780
|
+
const platform = args?.platform;
|
|
781
|
+
const stack = args?.stack;
|
|
782
|
+
const tokens = data.tokens;
|
|
783
|
+
const typography = tokens?.typography;
|
|
784
|
+
const fontFamily = typography?.fontFamily;
|
|
785
|
+
const fontNames = [];
|
|
786
|
+
if (fontFamily) {
|
|
787
|
+
for (const key of ["display", "heading", "body"]) {
|
|
788
|
+
const v = fontFamily[key];
|
|
789
|
+
if (typeof v === "string" && v && !fontNames.includes(v)) fontNames.push(v);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
const icons = tokens?.icons;
|
|
793
|
+
const ICON_PACKAGES = {
|
|
794
|
+
lucide: { web: "lucide-react", native: "lucide-react-native" },
|
|
795
|
+
heroicons: { web: "@heroicons/react", native: "heroicons-react-native" },
|
|
796
|
+
phosphor: { web: "phosphor-react", native: "phosphor-react-native" }
|
|
797
|
+
};
|
|
798
|
+
const lib = icons?.library || "lucide";
|
|
799
|
+
const iconPkgs = ICON_PACKAGES[lib] || ICON_PACKAGES.lucide;
|
|
800
|
+
const serverName = data.meta.name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
801
|
+
const npxArgs = ["@atomixstudio/mcp@latest"];
|
|
802
|
+
if (dsId) npxArgs.push("--ds-id", dsId);
|
|
803
|
+
if (apiKey) npxArgs.push("--api-key", apiKey);
|
|
804
|
+
const mcpConfig = {
|
|
805
|
+
path: ".cursor/mcp.json",
|
|
806
|
+
content: JSON.stringify({
|
|
807
|
+
mcpServers: {
|
|
808
|
+
[serverName]: { command: "npx", args: npxArgs }
|
|
809
|
+
}
|
|
810
|
+
}, null, 2)
|
|
811
|
+
};
|
|
812
|
+
const payload = {
|
|
813
|
+
iconLibrary: {
|
|
814
|
+
package: iconPkgs.web,
|
|
815
|
+
nativePackage: iconPkgs.native,
|
|
816
|
+
performanceHint: "Use individual SVG imports for tree-shaking; avoid loading entire icon libraries or icon fonts."
|
|
817
|
+
},
|
|
818
|
+
fonts: {
|
|
819
|
+
families: fontNames,
|
|
820
|
+
performanceHint: "Implement self-hosted fonts and local @font-face declarations; avoid external CDN links; use font-display: swap."
|
|
821
|
+
},
|
|
822
|
+
skill: {
|
|
823
|
+
path: ".cursor/skills/atomix-ds/SKILL.md",
|
|
824
|
+
content: GENERIC_SKILL_MD
|
|
825
|
+
},
|
|
826
|
+
mcpConfig,
|
|
827
|
+
tokenFiles: {
|
|
828
|
+
files: ["tokens.css", "tokens.json"],
|
|
829
|
+
copyInstructions: "Copy tokens.css and tokens.json from the design system export (or use syncTokens) into your project."
|
|
830
|
+
},
|
|
831
|
+
meta: { dsName: data.meta.name, platform: platform ?? void 0, stack: stack ?? void 0 }
|
|
832
|
+
};
|
|
833
|
+
return {
|
|
834
|
+
content: [{
|
|
835
|
+
type: "text",
|
|
836
|
+
text: JSON.stringify(payload, null, 2)
|
|
837
|
+
}]
|
|
838
|
+
};
|
|
839
|
+
}
|
|
693
840
|
default:
|
|
694
841
|
return {
|
|
695
842
|
content: [{
|
|
696
843
|
type: "text",
|
|
697
844
|
text: JSON.stringify({
|
|
698
845
|
error: `Unknown tool: ${name}`,
|
|
699
|
-
availableTools: ["getToken", "listTokens", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncTokens"]
|
|
846
|
+
availableTools: ["getToken", "listTokens", "searchTokens", "validateUsage", "getAIToolRules", "exportMCPConfig", "getSetupInstructions", "syncTokens", "getDependencies"]
|
|
700
847
|
}, null, 2)
|
|
701
848
|
}]
|
|
702
849
|
};
|
|
703
850
|
}
|
|
704
851
|
} catch (error) {
|
|
852
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
853
|
+
let suggestion = "Check your MCP server configuration.";
|
|
854
|
+
if (errorMessage.includes("Missing --ds-id")) {
|
|
855
|
+
suggestion = "Add --ds-id <your-design-system-id> to your MCP server configuration.";
|
|
856
|
+
} else if (errorMessage.includes("Failed to fetch")) {
|
|
857
|
+
const statusMatch = errorMessage.match(/Failed to fetch design system: (\d+)/);
|
|
858
|
+
if (statusMatch) {
|
|
859
|
+
const status = statusMatch[1];
|
|
860
|
+
if (status === "404") {
|
|
861
|
+
suggestion = `Design system ID "${dsId}" not found. Verify the ID is correct or the design system has been published.`;
|
|
862
|
+
} else if (status === "401" || status === "403") {
|
|
863
|
+
suggestion = "Design system is private. Add --api-key <your-key> to your MCP server configuration.";
|
|
864
|
+
} else {
|
|
865
|
+
suggestion = `API request failed (${status}). Check your network connection and API base URL (${apiBase}).`;
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
suggestion = `Failed to connect to API at ${apiBase}. Check your network connection.`;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
705
871
|
return {
|
|
706
872
|
content: [{
|
|
707
873
|
type: "text",
|
|
708
874
|
text: JSON.stringify({
|
|
709
|
-
error:
|
|
710
|
-
suggestion
|
|
875
|
+
error: errorMessage,
|
|
876
|
+
suggestion,
|
|
877
|
+
configured: {
|
|
878
|
+
dsId: dsId || "not set",
|
|
879
|
+
apiBase: apiBase || "not set",
|
|
880
|
+
hasApiKey: !!apiKey
|
|
881
|
+
}
|
|
711
882
|
}, null, 2)
|
|
712
883
|
}],
|
|
713
884
|
isError: true
|
|
714
885
|
};
|
|
715
886
|
}
|
|
716
887
|
});
|
|
888
|
+
var GENERIC_SKILL_MD = `---
|
|
889
|
+
name: atomix-ds
|
|
890
|
+
description: Use this design system when editing UI, design system files, or when the user asks to follow the project's design system. Fetch rules dynamically via MCP.
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
# Atomix Design System
|
|
894
|
+
|
|
895
|
+
## When to use
|
|
896
|
+
|
|
897
|
+
- Editing UI components, pages, or styles
|
|
898
|
+
- Working in design system or token files
|
|
899
|
+
- User asks to follow the project's design system
|
|
900
|
+
|
|
901
|
+
## How to use
|
|
902
|
+
|
|
903
|
+
1. Call MCP tool **getAIToolRules** with the tool id that matches your **current AI coding environment**:
|
|
904
|
+
- **cursor** (Cursor IDE)
|
|
905
|
+
- **windsurf** (Windsurf)
|
|
906
|
+
- **copilot** (Copilot)
|
|
907
|
+
- **cline** (Cline)
|
|
908
|
+
- **continue** (Continue)
|
|
909
|
+
- **zed** (Zed)
|
|
910
|
+
- **generic** (other tools)
|
|
911
|
+
|
|
912
|
+
Example: \`getAIToolRules({ tool: "cursor" })\` when in Cursor.
|
|
913
|
+
|
|
914
|
+
2. Alternatively use the **design-system-rules** prompt or the resource \`atomix://rules/<tool>\` with the same tool id.
|
|
915
|
+
|
|
916
|
+
3. Apply the returned rules (tokens, governance) when generating or editing code.
|
|
917
|
+
|
|
918
|
+
4. Ensure Atomix MCP is configured with the project's \`--ds-id\` (config path varies by platform: e.g. \`.cursor/mcp.json\`, \`.windsurf/mcp.json\`).
|
|
919
|
+
`;
|
|
717
920
|
var AI_TOOLS = ["cursor", "copilot", "windsurf", "cline", "continue", "zed", "generic"];
|
|
718
921
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
922
|
+
try {
|
|
923
|
+
const data = await fetchDesignSystemForMCP();
|
|
924
|
+
const stats = getTokenStats(data);
|
|
925
|
+
const resources = [
|
|
926
|
+
{
|
|
927
|
+
uri: "atomix://hello",
|
|
928
|
+
name: `Welcome to ${data.meta.name}`,
|
|
929
|
+
description: `Design system overview with ${stats.total} tokens and ${stats.governanceRules} governance rules`,
|
|
930
|
+
mimeType: "text/markdown"
|
|
931
|
+
},
|
|
932
|
+
...AI_TOOLS.map((tool) => ({
|
|
933
|
+
uri: `atomix://rules/${tool}`,
|
|
934
|
+
name: `${tool.charAt(0).toUpperCase() + tool.slice(1)} Rules`,
|
|
935
|
+
description: `Design system rules file for ${tool}`,
|
|
936
|
+
mimeType: "text/markdown"
|
|
937
|
+
}))
|
|
938
|
+
];
|
|
939
|
+
return { resources };
|
|
940
|
+
} catch {
|
|
941
|
+
return {
|
|
942
|
+
resources: [
|
|
943
|
+
{
|
|
944
|
+
uri: "atomix://setup",
|
|
945
|
+
name: "Configure MCP",
|
|
946
|
+
description: "Add --ds-id to your MCP config to load design system resources",
|
|
947
|
+
mimeType: "text/markdown"
|
|
948
|
+
}
|
|
949
|
+
]
|
|
950
|
+
};
|
|
951
|
+
}
|
|
736
952
|
});
|
|
737
953
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
738
954
|
const { uri } = request.params;
|
|
739
|
-
|
|
955
|
+
if (uri === "atomix://setup") {
|
|
956
|
+
return {
|
|
957
|
+
contents: [{
|
|
958
|
+
uri,
|
|
959
|
+
mimeType: "text/markdown",
|
|
960
|
+
text: `# Configure Atomix MCP
|
|
961
|
+
|
|
962
|
+
Add \`--ds-id <your-design-system-id>\` to your MCP server config.
|
|
963
|
+
|
|
964
|
+
Example (\`.cursor/mcp.json\`):
|
|
965
|
+
\`\`\`json
|
|
966
|
+
{
|
|
967
|
+
"mcpServers": {
|
|
968
|
+
"atomix": {
|
|
969
|
+
"command": "npx",
|
|
970
|
+
"args": ["@atomixstudio/mcp@latest", "--ds-id", "<your-ds-id>"]
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
\`\`\`
|
|
975
|
+
|
|
976
|
+
Get your DS ID from: https://atomixstudio.eu/ds/[your-ds-id]`
|
|
977
|
+
}]
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
const data = await fetchDesignSystemForMCP();
|
|
740
981
|
const stats = getTokenStats(data);
|
|
741
|
-
if (uri === "atomix://
|
|
982
|
+
if (uri === "atomix://hello") {
|
|
742
983
|
const welcome = generateWelcomeMessage(data, stats);
|
|
743
984
|
return {
|
|
744
985
|
contents: [{
|
|
@@ -773,35 +1014,185 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
773
1014
|
throw new Error(`Unknown resource: ${uri}`);
|
|
774
1015
|
});
|
|
775
1016
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1017
|
+
const prompts = [
|
|
1018
|
+
{
|
|
1019
|
+
name: "hello",
|
|
1020
|
+
description: "Get started with this design system - shows overview, available tokens, and tools. Run this first!"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "design-system-rules",
|
|
1024
|
+
description: "Get the design system governance rules for your AI coding tool",
|
|
1025
|
+
arguments: [
|
|
1026
|
+
{
|
|
1027
|
+
name: "tool",
|
|
1028
|
+
description: "AI tool to generate rules for (cursor, copilot, windsurf, cline, continue, zed, generic)",
|
|
1029
|
+
required: false
|
|
1030
|
+
}
|
|
1031
|
+
]
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
name: "spacing",
|
|
1035
|
+
description: "List all spacing tokens with their values in a table format"
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "radius",
|
|
1039
|
+
description: "List all border radius tokens with their values in a table format"
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
name: "color",
|
|
1043
|
+
description: "List all color tokens with light/dark mode values in a table format"
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: "typography",
|
|
1047
|
+
description: "List all typography tokens with their values in a table format"
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
name: "shadow",
|
|
1051
|
+
description: "List all shadow/elevation tokens with their values in a table format"
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: "border",
|
|
1055
|
+
description: "List all border width tokens with their values in a table format"
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
name: "sizing",
|
|
1059
|
+
description: "List all sizing tokens (height, icon) with their values in a table format"
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
name: "motion",
|
|
1063
|
+
description: "List all motion tokens (duration, easing) with their values in a table format"
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
name: "sync",
|
|
1067
|
+
description: "Sync design tokens (./tokens.css). Safe: Adds new tokens, updates existing values, marks deprecated tokens. Supports formats: css, scss, less, json, ts, js, swift, kotlin, dart. Use /refactor to migrate deprecated tokens."
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "refactor",
|
|
1071
|
+
description: "Migrate deprecated tokens in codebase. Scans for deprecated tokens marked by /sync and suggests replacements. Run after /sync to update code."
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
name: "install",
|
|
1075
|
+
description: "Suggest dependencies for this design system (icons, fonts, SKILL.md, MCP config, tokens). Asks before installing. Run getDependencies then follow phased instructions.",
|
|
1076
|
+
arguments: [
|
|
1077
|
+
{ name: "platform", description: "Target platform (web, ios, android). Optional; ask user if unknown.", required: false },
|
|
1078
|
+
{ name: "stack", description: "Stack or framework (e.g. react, vue, next, swift, kotlin). Optional.", required: false }
|
|
1079
|
+
]
|
|
1080
|
+
}
|
|
1081
|
+
];
|
|
1082
|
+
return { prompts };
|
|
1083
|
+
});
|
|
1084
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1085
|
+
const { name, arguments: args } = request.params;
|
|
1086
|
+
const shouldForceRefresh = name === "sync";
|
|
1087
|
+
let data = null;
|
|
1088
|
+
let stats = null;
|
|
1089
|
+
try {
|
|
1090
|
+
data = await fetchDesignSystemForMCP(shouldForceRefresh);
|
|
1091
|
+
stats = getTokenStats(data);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1094
|
+
if (errorMessage.includes("Missing --ds-id")) {
|
|
1095
|
+
return {
|
|
1096
|
+
description: "MCP Server Configuration Required",
|
|
1097
|
+
messages: [
|
|
786
1098
|
{
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1099
|
+
role: "user",
|
|
1100
|
+
content: {
|
|
1101
|
+
type: "text",
|
|
1102
|
+
text: `The MCP server isn't configured. To use design system prompts, you need to configure the server with:
|
|
1103
|
+
- \`--ds-id\`: Your design system ID (required - get it from https://atomixstudio.eu/ds/[your-ds-id])
|
|
1104
|
+
- \`--api-key\`: Your API key (optional - only needed for private design systems)
|
|
1105
|
+
|
|
1106
|
+
**Note:** Most design systems are public and don't require an API key. Only add \`--api-key\` if your design system is private.
|
|
1107
|
+
|
|
1108
|
+
Configure the MCP server in your Cursor settings, then restart Cursor.`
|
|
1109
|
+
}
|
|
790
1110
|
}
|
|
791
1111
|
]
|
|
1112
|
+
};
|
|
1113
|
+
} else if (errorMessage.includes("Failed to fetch")) {
|
|
1114
|
+
const statusMatch = errorMessage.match(/Failed to fetch design system: (\d+) (.+)/);
|
|
1115
|
+
const status = statusMatch ? statusMatch[1] : "unknown";
|
|
1116
|
+
const details = statusMatch ? statusMatch[2] : errorMessage;
|
|
1117
|
+
let helpText = `**Error:** Failed to fetch design system from ${apiBase}/api/ds/${dsId}/tokens
|
|
1118
|
+
|
|
1119
|
+
`;
|
|
1120
|
+
helpText += `**Status:** ${status}
|
|
1121
|
+
`;
|
|
1122
|
+
helpText += `**Details:** ${details}
|
|
1123
|
+
|
|
1124
|
+
`;
|
|
1125
|
+
if (status === "404") {
|
|
1126
|
+
helpText += `**Possible causes:**
|
|
1127
|
+
`;
|
|
1128
|
+
helpText += `- Design system ID "${dsId}" doesn't exist
|
|
1129
|
+
`;
|
|
1130
|
+
helpText += `- API base URL "${apiBase}" is incorrect
|
|
1131
|
+
`;
|
|
1132
|
+
helpText += `- Design system hasn't been published yet
|
|
1133
|
+
|
|
1134
|
+
`;
|
|
1135
|
+
helpText += `**Solution:** Verify the design system ID and API base URL in your MCP server configuration.`;
|
|
1136
|
+
} else if (status === "401" || status === "403") {
|
|
1137
|
+
helpText += `**Possible causes:**
|
|
1138
|
+
`;
|
|
1139
|
+
helpText += `- Design system is private and requires an API key
|
|
1140
|
+
`;
|
|
1141
|
+
helpText += `- API key is invalid or expired
|
|
1142
|
+
|
|
1143
|
+
`;
|
|
1144
|
+
helpText += `**Solution:** Add \`--api-key <your-key>\` to your MCP server configuration.`;
|
|
1145
|
+
} else {
|
|
1146
|
+
helpText += `**Possible causes:**
|
|
1147
|
+
`;
|
|
1148
|
+
helpText += `- Network connection issue
|
|
1149
|
+
`;
|
|
1150
|
+
helpText += `- API server is down
|
|
1151
|
+
`;
|
|
1152
|
+
helpText += `- CORS or firewall blocking the request
|
|
1153
|
+
|
|
1154
|
+
`;
|
|
1155
|
+
helpText += `**Solution:** Check your network connection and verify the API base URL is accessible.`;
|
|
792
1156
|
}
|
|
793
|
-
|
|
1157
|
+
return {
|
|
1158
|
+
description: "Design System Fetch Failed",
|
|
1159
|
+
messages: [
|
|
1160
|
+
{
|
|
1161
|
+
role: "user",
|
|
1162
|
+
content: {
|
|
1163
|
+
type: "text",
|
|
1164
|
+
text: helpText
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
]
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
const buildCategoryPrompt = (category, instructions) => {
|
|
1173
|
+
const lines = [];
|
|
1174
|
+
lines.push(`## Design System Information`);
|
|
1175
|
+
lines.push(`- **Name**: ${data.meta.name}`);
|
|
1176
|
+
lines.push(`- **Version**: ${data.meta.version || "1.0.0"}`);
|
|
1177
|
+
lines.push(`- **Last Published**: ${data.meta.exportedAt ? new Date(data.meta.exportedAt).toLocaleDateString() : "N/A"}`);
|
|
1178
|
+
lines.push(``);
|
|
1179
|
+
const categoryRules = data.governance.categories?.[category];
|
|
1180
|
+
if (categoryRules && categoryRules.length > 0) {
|
|
1181
|
+
lines.push(`## AI Rules for ${category.charAt(0).toUpperCase() + category.slice(1)}`);
|
|
1182
|
+
categoryRules.forEach((rule, index) => {
|
|
1183
|
+
lines.push(`${index + 1}. ${rule}`);
|
|
1184
|
+
});
|
|
1185
|
+
lines.push(``);
|
|
1186
|
+
}
|
|
1187
|
+
lines.push(`## Instructions`);
|
|
1188
|
+
lines.push(instructions);
|
|
1189
|
+
return lines.join("\n");
|
|
794
1190
|
};
|
|
795
|
-
});
|
|
796
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
797
|
-
const { name, arguments: args } = request.params;
|
|
798
|
-
const data = await fetchDesignSystem();
|
|
799
|
-
const stats = getTokenStats(data);
|
|
800
1191
|
switch (name) {
|
|
801
|
-
case "
|
|
1192
|
+
case "hello": {
|
|
802
1193
|
const welcome = generateWelcomeMessage(data, stats);
|
|
803
1194
|
return {
|
|
804
|
-
description: `
|
|
1195
|
+
description: `Hello \u2014 ${data.meta.name} Design System`,
|
|
805
1196
|
messages: [
|
|
806
1197
|
{
|
|
807
1198
|
role: "user",
|
|
@@ -850,33 +1241,345 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
850
1241
|
]
|
|
851
1242
|
};
|
|
852
1243
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1244
|
+
case "spacing": {
|
|
1245
|
+
const instructions = `List all spacing tokens in a table format. Use the listTokens tool with category "spacing" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "spacing.xs" instead of "spacing.scale.xs").`;
|
|
1246
|
+
const response = {
|
|
1247
|
+
description: "List all spacing tokens",
|
|
1248
|
+
messages: [
|
|
1249
|
+
{
|
|
1250
|
+
role: "user",
|
|
1251
|
+
content: {
|
|
1252
|
+
type: "text",
|
|
1253
|
+
text: buildCategoryPrompt("spacing", instructions)
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
]
|
|
1257
|
+
};
|
|
1258
|
+
return response;
|
|
1259
|
+
}
|
|
1260
|
+
case "radius": {
|
|
1261
|
+
const instructions = `List all border radius tokens in a table format. Use the listTokens tool with category "radius" and subcategory "scale". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "radius.sm" instead of "radius.scale.sm").`;
|
|
1262
|
+
return {
|
|
1263
|
+
description: "List all border radius tokens",
|
|
1264
|
+
messages: [
|
|
1265
|
+
{
|
|
1266
|
+
role: "user",
|
|
1267
|
+
content: {
|
|
1268
|
+
type: "text",
|
|
1269
|
+
text: buildCategoryPrompt("radius", instructions)
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
]
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
case "color": {
|
|
1276
|
+
const instructions = `List all color tokens in a table format showing both light and dark mode values.
|
|
1277
|
+
|
|
1278
|
+
First, call listTokens with category "colors" and subcategory "modes.light" to get light mode colors.
|
|
1279
|
+
Then, call listTokens with category "colors" and subcategory "modes.dark" to get dark mode colors.
|
|
1280
|
+
Also call listTokens with category "colors" and subcategory "static.brand" to get brand colors.
|
|
1281
|
+
|
|
1282
|
+
Format the response as a markdown table with columns: Token Name | Light Mode Value | Dark Mode Value | CSS Variable.
|
|
1283
|
+
|
|
1284
|
+
For brand colors (from static.brand), show the same value in both Light and Dark columns since brand colors don't change with mode.
|
|
1285
|
+
For semantic colors (from modes.light/modes.dark), match tokens by name and show their respective values.
|
|
1286
|
+
|
|
1287
|
+
The Token Name should be in short format:
|
|
1288
|
+
- Brand colors: "colors.brand.primary" (not "colors.static.brand.primary")
|
|
1289
|
+
- Semantic colors: "colors.bgSurface" (not "colors.modes.light.bgSurface")`;
|
|
1290
|
+
return {
|
|
1291
|
+
description: "List all color tokens with light/dark mode",
|
|
1292
|
+
messages: [
|
|
1293
|
+
{
|
|
1294
|
+
role: "user",
|
|
1295
|
+
content: {
|
|
1296
|
+
type: "text",
|
|
1297
|
+
text: buildCategoryPrompt("colors", instructions)
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
]
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
case "typography": {
|
|
1304
|
+
const instructions = `List all typography tokens in a table format. Use the listTokens tool with category "typography" (no subcategory needed). Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group tokens by type (fontSize, fontWeight, lineHeight, etc.) with section headers.`;
|
|
1305
|
+
return {
|
|
1306
|
+
description: "List all typography tokens",
|
|
1307
|
+
messages: [
|
|
1308
|
+
{
|
|
1309
|
+
role: "user",
|
|
1310
|
+
content: {
|
|
1311
|
+
type: "text",
|
|
1312
|
+
text: buildCategoryPrompt("typography", instructions)
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
]
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
case "shadow": {
|
|
1319
|
+
const instructions = `List all shadow/elevation tokens in a table format. Use the listTokens tool with category "shadows" and subcategory "elevation". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "shadows.elevation.md" is fine as-is).`;
|
|
1320
|
+
return {
|
|
1321
|
+
description: "List all shadow/elevation tokens",
|
|
1322
|
+
messages: [
|
|
1323
|
+
{
|
|
1324
|
+
role: "user",
|
|
1325
|
+
content: {
|
|
1326
|
+
type: "text",
|
|
1327
|
+
text: buildCategoryPrompt("shadows", instructions)
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
]
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
case "border": {
|
|
1334
|
+
const instructions = `List all border width tokens in a table format. Use the listTokens tool with category "borders" and subcategory "width". Format the response as a markdown table with columns: Token Name | Value | CSS Variable. The Token Name should be in short format (e.g., "borders.width.sm" is fine as-is).`;
|
|
1335
|
+
return {
|
|
1336
|
+
description: "List all border width tokens",
|
|
1337
|
+
messages: [
|
|
1338
|
+
{
|
|
1339
|
+
role: "user",
|
|
1340
|
+
content: {
|
|
1341
|
+
type: "text",
|
|
1342
|
+
text: buildCategoryPrompt("borders", instructions)
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
]
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
case "sizing": {
|
|
1349
|
+
const instructions = `List all sizing tokens in a table format. Call listTokens twice:
|
|
1350
|
+
1. category "sizing" and subcategory "height" for component heights
|
|
1351
|
+
2. category "sizing" and subcategory "icon" for icon sizes
|
|
1352
|
+
|
|
1353
|
+
Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (height vs icon) with section headers.`;
|
|
1354
|
+
return {
|
|
1355
|
+
description: "List all sizing tokens",
|
|
1356
|
+
messages: [
|
|
1357
|
+
{
|
|
1358
|
+
role: "user",
|
|
1359
|
+
content: {
|
|
1360
|
+
type: "text",
|
|
1361
|
+
text: buildCategoryPrompt("sizing", instructions)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
]
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
case "motion": {
|
|
1368
|
+
const instructions = `List all motion tokens in a table format. Call listTokens twice:
|
|
1369
|
+
1. category "motion" and subcategory "duration" for duration tokens
|
|
1370
|
+
2. category "motion" and subcategory "easing" for easing tokens
|
|
1371
|
+
|
|
1372
|
+
Format the response as a markdown table with columns: Token Name | Value | CSS Variable. Group by type (duration vs easing) with section headers.`;
|
|
1373
|
+
return {
|
|
1374
|
+
description: "List all motion tokens",
|
|
1375
|
+
messages: [
|
|
1376
|
+
{
|
|
1377
|
+
role: "user",
|
|
1378
|
+
content: {
|
|
1379
|
+
type: "text",
|
|
1380
|
+
text: buildCategoryPrompt("motion", instructions)
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
]
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
case "sync": {
|
|
1387
|
+
const output = args?.output || "./tokens.css";
|
|
1388
|
+
const format = args?.format || "css";
|
|
1389
|
+
const response = {
|
|
1390
|
+
description: `Sync design tokens to ${output}`,
|
|
1391
|
+
messages: [
|
|
1392
|
+
{
|
|
1393
|
+
role: "user",
|
|
1394
|
+
content: {
|
|
1395
|
+
type: "text",
|
|
1396
|
+
text: `Call the syncTokens tool now. Use output="${output}" and format="${format}". Execute immediately - do not search or ask questions.`
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
]
|
|
1400
|
+
};
|
|
1401
|
+
return response;
|
|
1402
|
+
}
|
|
1403
|
+
case "refactor": {
|
|
1404
|
+
if (!lastSyncAffectedTokens) {
|
|
1405
|
+
return {
|
|
1406
|
+
description: "Refactor codebase for deprecated tokens",
|
|
1407
|
+
messages: [
|
|
1408
|
+
{
|
|
1409
|
+
role: "user",
|
|
1410
|
+
content: {
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: `No recent sync data available. Please run \`/sync\` first to update your token file, then run \`/refactor\` to scan your codebase for deprecated token usage.`
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
]
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
if (lastSyncAffectedTokens.removed.length === 0) {
|
|
1419
|
+
return {
|
|
1420
|
+
description: "Refactor codebase for deprecated tokens",
|
|
1421
|
+
messages: [
|
|
1422
|
+
{
|
|
1423
|
+
role: "user",
|
|
1424
|
+
content: {
|
|
1425
|
+
type: "text",
|
|
1426
|
+
text: `\u2713 No deprecated tokens found in last sync.
|
|
1427
|
+
|
|
1428
|
+
Your codebase is up to date! The last sync on ${new Date(lastSyncAffectedTokens.timestamp).toLocaleString()} did not remove any tokens.`
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
]
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
const format = lastSyncAffectedTokens.format;
|
|
1435
|
+
const isNativeFormat = ["swift", "kotlin", "dart"].includes(format);
|
|
1436
|
+
let searchPatterns = "";
|
|
1437
|
+
let fileExtensions = "";
|
|
1438
|
+
if (isNativeFormat) {
|
|
1439
|
+
const nativeTokens = lastSyncAffectedTokens.removed.map((r) => {
|
|
1440
|
+
const withoutPrefix = r.token.replace(/^--atmx-/, "");
|
|
1441
|
+
const camelCase = withoutPrefix.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1442
|
+
const pascalCase = camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
1443
|
+
return { css: r.token, camel: camelCase, pascal: pascalCase, lastValue: r.lastValue };
|
|
1444
|
+
});
|
|
1445
|
+
if (format === "swift") {
|
|
1446
|
+
fileExtensions = ".swift files";
|
|
1447
|
+
searchPatterns = nativeTokens.map((t) => ` \u2022 \`DesignTokens.*.${t.camel}\` (was: ${t.lastValue})`).join("\n");
|
|
1448
|
+
} else if (format === "kotlin") {
|
|
1449
|
+
fileExtensions = ".kt files";
|
|
1450
|
+
searchPatterns = nativeTokens.map((t) => ` \u2022 \`DesignTokens.*.${t.pascal}\` (was: ${t.lastValue})`).join("\n");
|
|
1451
|
+
} else {
|
|
1452
|
+
fileExtensions = ".dart files";
|
|
1453
|
+
searchPatterns = nativeTokens.map((t) => ` \u2022 \`DesignTokens.${t.camel}\` (was: ${t.lastValue})`).join("\n");
|
|
1454
|
+
}
|
|
1455
|
+
} else {
|
|
1456
|
+
fileExtensions = ".tsx, .ts, .jsx, .js, .css, .scss, .less, .vue, .svelte files";
|
|
1457
|
+
searchPatterns = lastSyncAffectedTokens.removed.map(
|
|
1458
|
+
(r) => ` \u2022 \`var(${r.token})\` or \`"${r.token}"\` (was: ${r.lastValue})`
|
|
1459
|
+
).join("\n");
|
|
1460
|
+
}
|
|
1461
|
+
const instructions = `Scan the codebase for deprecated design tokens and help update them.
|
|
1462
|
+
|
|
1463
|
+
## Deprecated Tokens (${lastSyncAffectedTokens.removed.length})
|
|
1464
|
+
|
|
1465
|
+
The following tokens have been removed from the design system:
|
|
1466
|
+
|
|
1467
|
+
${searchPatterns}
|
|
1468
|
+
|
|
1469
|
+
## Instructions
|
|
1470
|
+
|
|
1471
|
+
1. **Search** for these patterns in ${fileExtensions}:
|
|
1472
|
+
- Skip node_modules, dist, build, .next, .git directories
|
|
1473
|
+
- Look for both \`var(--token)\` and direct string references
|
|
1474
|
+
|
|
1475
|
+
2. **Report findings** in this format:
|
|
1476
|
+
\`\`\`
|
|
1477
|
+
\u{1F4C4} path/to/file.tsx
|
|
1478
|
+
Line 42: background: var(--atmx-color-deprecated);
|
|
1479
|
+
Line 78: const color = "--atmx-color-deprecated";
|
|
1480
|
+
\`\`\`
|
|
1481
|
+
|
|
1482
|
+
3. **After listing all findings**, ask:
|
|
1483
|
+
> "Found X instances of deprecated tokens in Y files. Would you like me to update them?"
|
|
1484
|
+
>
|
|
1485
|
+
> For each deprecated token, I'll suggest the closest equivalent from the current design system.
|
|
1486
|
+
|
|
1487
|
+
4. **If user confirms**:
|
|
1488
|
+
- Show the proposed replacement for each instance
|
|
1489
|
+
- Apply changes only after user approves
|
|
1490
|
+
- For tokens with no clear replacement, ask user which token to use
|
|
1491
|
+
|
|
1492
|
+
## Important
|
|
1493
|
+
|
|
1494
|
+
- Do NOT make any changes without explicit user confirmation
|
|
1495
|
+
- Show before \u2192 after for each change
|
|
1496
|
+
- If you can't find a suitable replacement token, ask the user
|
|
1497
|
+
|
|
1498
|
+
## Available Token Categories
|
|
1499
|
+
|
|
1500
|
+
Use \`/color\`, \`/spacing\`, \`/radius\`, \`/typography\`, \`/shadow\`, \`/border\`, \`/sizing\`, \`/motion\` to see current tokens if you need to find replacements.`;
|
|
1501
|
+
return {
|
|
1502
|
+
description: "Refactor codebase for deprecated tokens",
|
|
1503
|
+
messages: [
|
|
1504
|
+
{
|
|
1505
|
+
role: "user",
|
|
1506
|
+
content: {
|
|
1507
|
+
type: "text",
|
|
1508
|
+
text: instructions
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
]
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
case "install": {
|
|
1515
|
+
const installInstructions = `You are running **atomix/install**. Follow these phases in order; do not skip any phase.
|
|
1516
|
+
|
|
1517
|
+
## Phase 1 \u2013 Resolve platform and stack
|
|
1518
|
+
|
|
1519
|
+
- If the project clearly indicates platform (e.g. package.json + web deps, or build.gradle / Android, or Xcode / iOS), infer \`platform\` (e.g. web, ios, android) and optionally \`stack\` (e.g. react, next, vue, swift, kotlin).
|
|
1520
|
+
- If the project gives **no hint** (blank repo, empty folder, or ambiguous): **Ask the user:** "Which platform are you building for? (e.g. web, Android, iOS)" and, if relevant, "Which stack or framework? (e.g. React, Vue, Next, Swift, Kotlin)." Do **not** assume web or any default.
|
|
1521
|
+
- Proceed only once platform (and optionally stack) is known or confirmed.
|
|
1522
|
+
|
|
1523
|
+
## Phase 2 \u2013 Get suggested dependencies
|
|
1524
|
+
|
|
1525
|
+
- Call MCP tool **getDependencies** with the resolved \`platform\` (and optional \`stack\`). Use the parameter names the tool expects (e.g. platform, stack).
|
|
1526
|
+
- If the call fails (e.g. MCP not connected or no ds-id), tell the user: "Atomix MCP is not connected or design system ID is missing. Configure MCP with --ds-id and try again."
|
|
1527
|
+
|
|
1528
|
+
## Phase 3 \u2013 Scan codebase and build suggestion list
|
|
1529
|
+
|
|
1530
|
+
- Scan the repo for: package.json (or equivalent), existing skill path (e.g. .cursor/skills), presence of tokens.css or project token file, font imports/config, icon package usage.
|
|
1531
|
+
- Build two lists: **Suggested dependencies** (from getDependencies, minus what is already present) and **Already present**. Include: icon package, fonts, SKILL path, MCP config, token files.
|
|
1532
|
+
- Do **not** install, copy, or write any file in this phase.
|
|
1533
|
+
|
|
1534
|
+
## Phase 4 \u2013 Present list and ask before install
|
|
1535
|
+
|
|
1536
|
+
- Reply with: "Suggested dependencies: \u2026" and "Already present: \u2026" and state: "Do not install or copy anything until you confirm. Would you like me to install or add these?"
|
|
1537
|
+
- If the user says yes, perform only the steps they approved. If no or only part, perform only that.
|
|
1538
|
+
|
|
1539
|
+
## Phase 5 \u2013 Optional: suggest global styles (if missing)
|
|
1540
|
+
|
|
1541
|
+
- After presenting the dependency list (Phase 4), or after the user has approved install, check whether the project has **global styles that use the design system tokens** (e.g. typography scale, semantic color classes, or theme file referencing DS tokens). If **not** (e.g. no globals.css using tokens, or blank project), ask the user **verbatim**:
|
|
1542
|
+
|
|
1543
|
+
"Your project doesn't appear to have global styles that use the design system tokens (e.g. semantic typography scale or semantic color classes). Would you like me to suggest or generate a minimal set (e.g. typography scale + semantic utilities) so you can develop consistently with the DS?"
|
|
1544
|
+
|
|
1545
|
+
- If the user says yes, suggest or generate a minimal set; do not overwrite existing global styles without confirmation.
|
|
1546
|
+
|
|
1547
|
+
## Phase 6 \u2013 Report what was created
|
|
1548
|
+
|
|
1549
|
+
- After any install or copy steps (and optional global-styles step), list **what was created or updated** (e.g. "Installed: lucide-react. Added: .cursor/skills/atomix-ds/SKILL.md. Updated: .cursor/mcp.json. Copied: tokens.css."). If global styles were added, include that.
|
|
1550
|
+
|
|
1551
|
+
---
|
|
1552
|
+
|
|
1553
|
+
Execute Phase 1 now (resolve or ask platform/stack), then Phase 2 (call getDependencies), then continue through Phase 6.`;
|
|
1554
|
+
return {
|
|
1555
|
+
description: "Suggest dependencies for this design system (atomix/install)",
|
|
1556
|
+
messages: [
|
|
1557
|
+
{
|
|
1558
|
+
role: "user",
|
|
1559
|
+
content: {
|
|
1560
|
+
type: "text",
|
|
1561
|
+
text: installInstructions
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
]
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
default:
|
|
1568
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
function generateWelcomeMessage(data, stats) {
|
|
1572
|
+
const tokenSummary = Object.entries(stats.byCategory).map(([cat, count]) => `${cat}: ${count}`).join(", ");
|
|
1573
|
+
const asciiArt = `
|
|
1574
|
+
\`\`\`
|
|
1575
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1576
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1577
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1578
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1579
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1580
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1581
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
1582
|
+
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
880
1583
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
881
1584
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
882
1585
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198 \u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
@@ -890,7 +1593,6 @@ function generateWelcomeMessage(data, stats) {
|
|
|
890
1593
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
891
1594
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
892
1595
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
893
|
-
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
894
1596
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
895
1597
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
896
1598
|
\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198\u2198
|
|
@@ -899,8 +1601,6 @@ function generateWelcomeMessage(data, stats) {
|
|
|
899
1601
|
return `${asciiArt}
|
|
900
1602
|
# Welcome to ${data.meta.name}
|
|
901
1603
|
|
|
902
|
-
Your design system is connected and ready to use.
|
|
903
|
-
|
|
904
1604
|
## Design System Overview
|
|
905
1605
|
|
|
906
1606
|
| Metric | Value |
|
|
@@ -912,930 +1612,46 @@ Your design system is connected and ready to use.
|
|
|
912
1612
|
| Governance Rules | ${stats.governanceRules} |
|
|
913
1613
|
| Version | ${data.meta.version || 1} |
|
|
914
1614
|
|
|
915
|
-
### Token
|
|
916
|
-
|
|
917
|
-
${categoryBreakdown}
|
|
918
|
-
|
|
919
|
-
## Available Tools
|
|
920
|
-
|
|
921
|
-
${toolsList.map((t, i) => `${i + 1}. **${t.split(" - ")[0]}** - ${t.split(" - ")[1]}`).join("\n")}
|
|
922
|
-
|
|
923
|
-
## Quick Start
|
|
1615
|
+
### Token breakdown
|
|
924
1616
|
|
|
925
|
-
|
|
926
|
-
\`\`\`
|
|
927
|
-
Use getToken with path "colors.static.brand.primary"
|
|
928
|
-
\`\`\`
|
|
929
|
-
|
|
930
|
-
### List All Spacing Tokens
|
|
931
|
-
\`\`\`
|
|
932
|
-
Use listTokens with category "spacing"
|
|
933
|
-
\`\`\`
|
|
1617
|
+
${tokenSummary}
|
|
934
1618
|
|
|
935
|
-
|
|
936
|
-
\`\`\`
|
|
937
|
-
Use validateUsage with value "#ff0000" and context "color"
|
|
938
|
-
\`\`\`
|
|
939
|
-
|
|
940
|
-
## Resources Available
|
|
1619
|
+
---
|
|
941
1620
|
|
|
942
|
-
|
|
1621
|
+
## Essential commands
|
|
943
1622
|
|
|
944
|
-
|
|
|
945
|
-
|
|
946
|
-
|
|
|
947
|
-
|
|
|
948
|
-
|
|
|
949
|
-
| \`atomix://rules/windsurf\` | .windsurfrules file content |
|
|
950
|
-
| \`atomix://rules/cline\` | .clinerules file content |
|
|
951
|
-
| \`atomix://rules/continue\` | Continue rules |
|
|
952
|
-
| \`atomix://rules/zed\` | Zed assistant rules |
|
|
953
|
-
| \`atomix://rules/generic\` | Generic AI guidelines |
|
|
1623
|
+
| Command | What to expect |
|
|
1624
|
+
|---------|----------------|
|
|
1625
|
+
| **install** | Scans your repo to set up global styles (if none), icons and fonts. Safe: Won't install until you confirm. |
|
|
1626
|
+
| **sync** | Syncs tokens to a local file. Adds new, updates existing, marks deprecated. Safe: No changes until you confirm. |
|
|
1627
|
+
| **refactor** | Scans codebase for deprecated tokens (after sync), suggests replacements. Run after sync to migrate code. |
|
|
954
1628
|
|
|
955
|
-
##
|
|
1629
|
+
## Helper commands
|
|
956
1630
|
|
|
957
|
-
|
|
1631
|
+
| Command | What to expect |
|
|
1632
|
+
|---------|----------------|
|
|
1633
|
+
| **design-system-rules** | Returns governance rules for your AI tools. |
|
|
1634
|
+
| **spacing** | Table of all spacing tokens and values. |
|
|
1635
|
+
| **color** | Table of color tokens with light/dark values. |
|
|
1636
|
+
| **typography** | Table of typography tokens and values. |
|
|
1637
|
+
| **radius** | Table of border radius tokens. |
|
|
1638
|
+
| **shadow** | Table of shadow/elevation tokens. |
|
|
1639
|
+
| **border** | Table of border width tokens. |
|
|
1640
|
+
| **sizing** | Table of height and icon size tokens. |
|
|
1641
|
+
| **motion** | Table of duration and easing tokens. |
|
|
958
1642
|
|
|
959
|
-
|
|
960
|
-
2. **Refresh tokens**: Restart the MCP server to fetch latest tokens
|
|
961
|
-
3. **View changes**: Visit https://atomixstudio.eu/ds/${dsId} to see recent changes
|
|
1643
|
+
**Suggested next step:** Run **install** to set up global styles, icons, fonts, and token files; the AI will list options and ask before adding anything.
|
|
962
1644
|
|
|
963
1645
|
---
|
|
964
1646
|
|
|
965
|
-
*
|
|
966
|
-
`;
|
|
967
|
-
}
|
|
968
|
-
function findConfig() {
|
|
969
|
-
const configNames = [".atomixrc", ".atomixrc.json", "atomix.config.json"];
|
|
970
|
-
const cwd = process.cwd();
|
|
971
|
-
for (const name of configNames) {
|
|
972
|
-
const configPath = path.join(cwd, name);
|
|
973
|
-
if (fs.existsSync(configPath)) {
|
|
974
|
-
try {
|
|
975
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
976
|
-
return JSON.parse(content);
|
|
977
|
-
} catch {
|
|
978
|
-
console.error(`Error parsing ${name}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
return null;
|
|
983
|
-
}
|
|
984
|
-
function generateCSSOutput(cssVariables, darkModeColors) {
|
|
985
|
-
const lines = [
|
|
986
|
-
"/* Atomix Design System Tokens",
|
|
987
|
-
" * Auto-generated - do not edit manually",
|
|
988
|
-
` * Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
989
|
-
" */",
|
|
990
|
-
"",
|
|
991
|
-
"/* Light mode (default) */",
|
|
992
|
-
":root {"
|
|
993
|
-
];
|
|
994
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
995
|
-
lines.push(` ${key}: ${value};`);
|
|
996
|
-
}
|
|
997
|
-
lines.push("}");
|
|
998
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
999
|
-
lines.push("");
|
|
1000
|
-
lines.push("/* Dark mode */");
|
|
1001
|
-
lines.push(".dark,");
|
|
1002
|
-
lines.push('[data-theme="dark"] {');
|
|
1003
|
-
const firstKey = Object.keys(cssVariables)[0] || "--atmx-";
|
|
1004
|
-
const prefixMatch = firstKey.match(/^(--[a-z]+-)/);
|
|
1005
|
-
const prefix = prefixMatch ? prefixMatch[1] : "--atmx-";
|
|
1006
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1007
|
-
lines.push(` ${prefix}color-${key}: ${value};`);
|
|
1008
|
-
}
|
|
1009
|
-
lines.push("}");
|
|
1010
|
-
}
|
|
1011
|
-
lines.push("");
|
|
1012
|
-
return lines.join("\n");
|
|
1013
|
-
}
|
|
1014
|
-
function generateJSONOutput(tokens) {
|
|
1015
|
-
return JSON.stringify(tokens, null, 2);
|
|
1016
|
-
}
|
|
1017
|
-
function generateTSOutput(tokens) {
|
|
1018
|
-
return `// Atomix Design System Tokens
|
|
1019
|
-
// Auto-generated - do not edit manually
|
|
1020
|
-
// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1021
|
-
|
|
1022
|
-
export const tokens = ${JSON.stringify(tokens, null, 2)} as const;
|
|
1023
|
-
|
|
1024
|
-
export type Tokens = typeof tokens;
|
|
1647
|
+
*Atomix Studio \u2014 https://atomixstudio.eu | DS: https://atomixstudio.eu/ds/${dsId}*
|
|
1025
1648
|
`;
|
|
1026
1649
|
}
|
|
1027
|
-
function generateJSOutput(tokens) {
|
|
1028
|
-
return `// Atomix Design System Tokens
|
|
1029
|
-
// Auto-generated - do not edit manually
|
|
1030
|
-
// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1031
|
-
|
|
1032
|
-
export const tokens = ${JSON.stringify(tokens, null, 2)};
|
|
1033
|
-
`;
|
|
1034
|
-
}
|
|
1035
|
-
function generateSCSSOutput(cssVariables, darkModeColors) {
|
|
1036
|
-
const lines = [
|
|
1037
|
-
"// Atomix Design System Tokens",
|
|
1038
|
-
"// Auto-generated - do not edit manually",
|
|
1039
|
-
`// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1040
|
-
"",
|
|
1041
|
-
"// CSS Custom Properties (for use in CSS/HTML)",
|
|
1042
|
-
"// Light mode (default)",
|
|
1043
|
-
":root {"
|
|
1044
|
-
];
|
|
1045
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1046
|
-
lines.push(` ${key}: ${value};`);
|
|
1047
|
-
}
|
|
1048
|
-
lines.push("}");
|
|
1049
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1050
|
-
lines.push("");
|
|
1051
|
-
lines.push("// Dark mode");
|
|
1052
|
-
lines.push(".dark,");
|
|
1053
|
-
lines.push('[data-theme="dark"] {');
|
|
1054
|
-
const firstKey = Object.keys(cssVariables)[0] || "--atmx-";
|
|
1055
|
-
const prefixMatch = firstKey.match(/^(--[a-z]+-)/);
|
|
1056
|
-
const prefix = prefixMatch ? prefixMatch[1] : "--atmx-";
|
|
1057
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1058
|
-
lines.push(` ${prefix}color-${key}: ${value};`);
|
|
1059
|
-
}
|
|
1060
|
-
lines.push("}");
|
|
1061
|
-
}
|
|
1062
|
-
lines.push("");
|
|
1063
|
-
lines.push("// SCSS Variables (light mode values)");
|
|
1064
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1065
|
-
const scssVar = "$" + key.replace(/^--/, "");
|
|
1066
|
-
lines.push(`${scssVar}: ${value};`);
|
|
1067
|
-
}
|
|
1068
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1069
|
-
lines.push("");
|
|
1070
|
-
lines.push("// SCSS Variables (dark mode values)");
|
|
1071
|
-
const firstKey = Object.keys(cssVariables)[0] || "--atmx-";
|
|
1072
|
-
const prefixMatch = firstKey.match(/^--([a-z]+)-/);
|
|
1073
|
-
const prefix = prefixMatch ? prefixMatch[1] : "atmx";
|
|
1074
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1075
|
-
const scssVar = `$${prefix}-color-${key}-dark`;
|
|
1076
|
-
lines.push(`${scssVar}: ${value};`);
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
lines.push("");
|
|
1080
|
-
return lines.join("\n");
|
|
1081
|
-
}
|
|
1082
|
-
function generateLessOutput(cssVariables, darkModeColors) {
|
|
1083
|
-
const lines = [
|
|
1084
|
-
"// Atomix Design System Tokens",
|
|
1085
|
-
"// Auto-generated - do not edit manually",
|
|
1086
|
-
`// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1087
|
-
"",
|
|
1088
|
-
"// CSS Custom Properties (for use in CSS/HTML)",
|
|
1089
|
-
"// Light mode (default)",
|
|
1090
|
-
":root {"
|
|
1091
|
-
];
|
|
1092
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1093
|
-
lines.push(` ${key}: ${value};`);
|
|
1094
|
-
}
|
|
1095
|
-
lines.push("}");
|
|
1096
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1097
|
-
lines.push("");
|
|
1098
|
-
lines.push("// Dark mode");
|
|
1099
|
-
lines.push(".dark,");
|
|
1100
|
-
lines.push('[data-theme="dark"] {');
|
|
1101
|
-
const firstKey = Object.keys(cssVariables)[0] || "--atmx-";
|
|
1102
|
-
const prefixMatch = firstKey.match(/^(--[a-z]+-)/);
|
|
1103
|
-
const prefix = prefixMatch ? prefixMatch[1] : "--atmx-";
|
|
1104
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1105
|
-
lines.push(` ${prefix}color-${key}: ${value};`);
|
|
1106
|
-
}
|
|
1107
|
-
lines.push("}");
|
|
1108
|
-
}
|
|
1109
|
-
lines.push("");
|
|
1110
|
-
lines.push("// Less Variables (light mode values)");
|
|
1111
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1112
|
-
const lessVar = "@" + key.replace(/^--/, "");
|
|
1113
|
-
lines.push(`${lessVar}: ${value};`);
|
|
1114
|
-
}
|
|
1115
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1116
|
-
lines.push("");
|
|
1117
|
-
lines.push("// Less Variables (dark mode values)");
|
|
1118
|
-
const firstKey = Object.keys(cssVariables)[0] || "--atmx-";
|
|
1119
|
-
const prefixMatch = firstKey.match(/^--([a-z]+)-/);
|
|
1120
|
-
const prefix = prefixMatch ? prefixMatch[1] : "atmx";
|
|
1121
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1122
|
-
const lessVar = `@${prefix}-color-${key}-dark`;
|
|
1123
|
-
lines.push(`${lessVar}: ${value};`);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
lines.push("");
|
|
1127
|
-
return lines.join("\n");
|
|
1128
|
-
}
|
|
1129
|
-
function toSwiftName(cssVar) {
|
|
1130
|
-
return cssVar.replace(/^--atmx-/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1131
|
-
}
|
|
1132
|
-
function toKotlinName(cssVar) {
|
|
1133
|
-
const camel = toSwiftName(cssVar);
|
|
1134
|
-
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
1135
|
-
}
|
|
1136
|
-
function toDartName(cssVar) {
|
|
1137
|
-
return toSwiftName(cssVar);
|
|
1138
|
-
}
|
|
1139
|
-
function isColorValue(value) {
|
|
1140
|
-
return /^#[0-9A-Fa-f]{3,8}$/.test(value) || /^rgb/.test(value) || /^hsl/.test(value);
|
|
1141
|
-
}
|
|
1142
|
-
function hexToSwiftColor(hex) {
|
|
1143
|
-
const clean = hex.replace("#", "");
|
|
1144
|
-
if (clean.length === 3) {
|
|
1145
|
-
const r = clean[0], g = clean[1], b = clean[2];
|
|
1146
|
-
return `Color(hex: 0x${r}${r}${g}${g}${b}${b})`;
|
|
1147
|
-
}
|
|
1148
|
-
if (clean.length === 6) {
|
|
1149
|
-
return `Color(hex: 0x${clean})`;
|
|
1150
|
-
}
|
|
1151
|
-
if (clean.length === 8) {
|
|
1152
|
-
const rgb = clean.substring(0, 6);
|
|
1153
|
-
const alpha = parseInt(clean.substring(6, 8), 16) / 255;
|
|
1154
|
-
return `Color(hex: 0x${rgb}).opacity(${alpha.toFixed(2)})`;
|
|
1155
|
-
}
|
|
1156
|
-
return `Color.clear // Could not parse: ${hex}`;
|
|
1157
|
-
}
|
|
1158
|
-
function hexToKotlinColor(hex) {
|
|
1159
|
-
const clean = hex.replace("#", "").toUpperCase();
|
|
1160
|
-
if (clean.length === 3) {
|
|
1161
|
-
const r = clean[0], g = clean[1], b = clean[2];
|
|
1162
|
-
return `Color(0xFF${r}${r}${g}${g}${b}${b})`;
|
|
1163
|
-
}
|
|
1164
|
-
if (clean.length === 6) {
|
|
1165
|
-
return `Color(0xFF${clean})`;
|
|
1166
|
-
}
|
|
1167
|
-
if (clean.length === 8) {
|
|
1168
|
-
const rgb = clean.substring(0, 6);
|
|
1169
|
-
const alpha = clean.substring(6, 8);
|
|
1170
|
-
return `Color(0x${alpha}${rgb})`;
|
|
1171
|
-
}
|
|
1172
|
-
return `Color.Transparent // Could not parse: ${hex}`;
|
|
1173
|
-
}
|
|
1174
|
-
function hexToDartColor(hex) {
|
|
1175
|
-
const clean = hex.replace("#", "").toUpperCase();
|
|
1176
|
-
if (clean.length === 3) {
|
|
1177
|
-
const r = clean[0], g = clean[1], b = clean[2];
|
|
1178
|
-
return `Color(0xFF${r}${r}${g}${g}${b}${b})`;
|
|
1179
|
-
}
|
|
1180
|
-
if (clean.length === 6) {
|
|
1181
|
-
return `Color(0xFF${clean})`;
|
|
1182
|
-
}
|
|
1183
|
-
if (clean.length === 8) {
|
|
1184
|
-
const rgb = clean.substring(0, 6);
|
|
1185
|
-
const alpha = clean.substring(6, 8);
|
|
1186
|
-
return `Color(0x${alpha}${rgb})`;
|
|
1187
|
-
}
|
|
1188
|
-
return `Colors.transparent // Could not parse: ${hex}`;
|
|
1189
|
-
}
|
|
1190
|
-
function generateSwiftOutput(cssVariables, darkModeColors) {
|
|
1191
|
-
const lines = [
|
|
1192
|
-
"// Atomix Design System Tokens",
|
|
1193
|
-
"// Auto-generated - do not edit manually",
|
|
1194
|
-
`// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1195
|
-
"",
|
|
1196
|
-
"import SwiftUI",
|
|
1197
|
-
"",
|
|
1198
|
-
"// MARK: - Color Extension for Hex",
|
|
1199
|
-
"extension Color {",
|
|
1200
|
-
" init(hex: UInt, alpha: Double = 1.0) {",
|
|
1201
|
-
" self.init(",
|
|
1202
|
-
" .sRGB,",
|
|
1203
|
-
" red: Double((hex >> 16) & 0xFF) / 255.0,",
|
|
1204
|
-
" green: Double((hex >> 8) & 0xFF) / 255.0,",
|
|
1205
|
-
" blue: Double(hex & 0xFF) / 255.0,",
|
|
1206
|
-
" opacity: alpha",
|
|
1207
|
-
" )",
|
|
1208
|
-
" }",
|
|
1209
|
-
"}",
|
|
1210
|
-
"",
|
|
1211
|
-
"// MARK: - Design Tokens",
|
|
1212
|
-
"enum DesignTokens {",
|
|
1213
|
-
"",
|
|
1214
|
-
" // MARK: Colors (Light Mode)",
|
|
1215
|
-
" enum Colors {"
|
|
1216
|
-
];
|
|
1217
|
-
const colors = [];
|
|
1218
|
-
const spacing = [];
|
|
1219
|
-
const typography = [];
|
|
1220
|
-
const other = [];
|
|
1221
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1222
|
-
if (key.includes("-color-")) colors.push([key, value]);
|
|
1223
|
-
else if (key.includes("-spacing-")) spacing.push([key, value]);
|
|
1224
|
-
else if (key.includes("-typography-") || key.includes("-font-")) typography.push([key, value]);
|
|
1225
|
-
else other.push([key, value]);
|
|
1226
|
-
}
|
|
1227
|
-
for (const [key, value] of colors) {
|
|
1228
|
-
const name = toSwiftName(key);
|
|
1229
|
-
if (isColorValue(value)) {
|
|
1230
|
-
lines.push(` static let ${name} = ${hexToSwiftColor(value)}`);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
lines.push(" }");
|
|
1234
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1235
|
-
lines.push("");
|
|
1236
|
-
lines.push(" // MARK: Colors (Dark Mode)");
|
|
1237
|
-
lines.push(" enum ColorsDark {");
|
|
1238
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1239
|
-
const name = `color${key.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("")}`;
|
|
1240
|
-
if (isColorValue(value)) {
|
|
1241
|
-
lines.push(` static let ${name} = ${hexToSwiftColor(value)}`);
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
lines.push(" }");
|
|
1245
|
-
}
|
|
1246
|
-
lines.push("");
|
|
1247
|
-
lines.push(" // MARK: Spacing");
|
|
1248
|
-
lines.push(" enum Spacing {");
|
|
1249
|
-
for (const [key, value] of spacing) {
|
|
1250
|
-
const name = toSwiftName(key);
|
|
1251
|
-
const numValue = parseFloat(value);
|
|
1252
|
-
if (!isNaN(numValue)) {
|
|
1253
|
-
lines.push(` static let ${name}: CGFloat = ${numValue}`);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
lines.push(" }");
|
|
1257
|
-
lines.push("");
|
|
1258
|
-
lines.push(" // MARK: Typography");
|
|
1259
|
-
lines.push(" enum Typography {");
|
|
1260
|
-
for (const [key, value] of typography) {
|
|
1261
|
-
const name = toSwiftName(key);
|
|
1262
|
-
if (key.includes("size")) {
|
|
1263
|
-
const numValue = parseFloat(value);
|
|
1264
|
-
if (!isNaN(numValue)) {
|
|
1265
|
-
lines.push(` static let ${name}: CGFloat = ${numValue}`);
|
|
1266
|
-
}
|
|
1267
|
-
} else if (key.includes("weight")) {
|
|
1268
|
-
lines.push(` static let ${name} = "${value}"`);
|
|
1269
|
-
} else if (key.includes("family")) {
|
|
1270
|
-
lines.push(` static let ${name} = "${value}"`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
lines.push(" }");
|
|
1274
|
-
lines.push("");
|
|
1275
|
-
lines.push(" // MARK: Other");
|
|
1276
|
-
lines.push(" enum Other {");
|
|
1277
|
-
for (const [key, value] of other) {
|
|
1278
|
-
const name = toSwiftName(key);
|
|
1279
|
-
if (isColorValue(value)) {
|
|
1280
|
-
lines.push(` static let ${name} = ${hexToSwiftColor(value)}`);
|
|
1281
|
-
} else {
|
|
1282
|
-
const numValue = parseFloat(value);
|
|
1283
|
-
if (!isNaN(numValue)) {
|
|
1284
|
-
lines.push(` static let ${name}: CGFloat = ${numValue}`);
|
|
1285
|
-
} else {
|
|
1286
|
-
lines.push(` static let ${name} = "${value}"`);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
lines.push(" }");
|
|
1291
|
-
lines.push("}");
|
|
1292
|
-
lines.push("");
|
|
1293
|
-
return lines.join("\n");
|
|
1294
|
-
}
|
|
1295
|
-
function generateKotlinOutput(cssVariables, darkModeColors) {
|
|
1296
|
-
const lines = [
|
|
1297
|
-
"// Atomix Design System Tokens",
|
|
1298
|
-
"// Auto-generated - do not edit manually",
|
|
1299
|
-
`// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1300
|
-
"",
|
|
1301
|
-
"package com.atomix.design",
|
|
1302
|
-
"",
|
|
1303
|
-
"import androidx.compose.ui.graphics.Color",
|
|
1304
|
-
"import androidx.compose.ui.unit.dp",
|
|
1305
|
-
"import androidx.compose.ui.unit.sp",
|
|
1306
|
-
"",
|
|
1307
|
-
"object DesignTokens {",
|
|
1308
|
-
"",
|
|
1309
|
-
" // Light mode colors",
|
|
1310
|
-
" object Colors {"
|
|
1311
|
-
];
|
|
1312
|
-
const colors = [];
|
|
1313
|
-
const spacing = [];
|
|
1314
|
-
const typography = [];
|
|
1315
|
-
const other = [];
|
|
1316
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1317
|
-
if (key.includes("-color-")) colors.push([key, value]);
|
|
1318
|
-
else if (key.includes("-spacing-")) spacing.push([key, value]);
|
|
1319
|
-
else if (key.includes("-typography-") || key.includes("-font-")) typography.push([key, value]);
|
|
1320
|
-
else other.push([key, value]);
|
|
1321
|
-
}
|
|
1322
|
-
for (const [key, value] of colors) {
|
|
1323
|
-
const name = toKotlinName(key);
|
|
1324
|
-
if (isColorValue(value)) {
|
|
1325
|
-
lines.push(` val ${name} = ${hexToKotlinColor(value)}`);
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
lines.push(" }");
|
|
1329
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1330
|
-
lines.push("");
|
|
1331
|
-
lines.push(" // Dark mode colors");
|
|
1332
|
-
lines.push(" object ColorsDark {");
|
|
1333
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1334
|
-
const name = `Color${key.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("")}`;
|
|
1335
|
-
if (isColorValue(value)) {
|
|
1336
|
-
lines.push(` val ${name} = ${hexToKotlinColor(value)}`);
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
lines.push(" }");
|
|
1340
|
-
}
|
|
1341
|
-
lines.push("");
|
|
1342
|
-
lines.push(" object Spacing {");
|
|
1343
|
-
for (const [key, value] of spacing) {
|
|
1344
|
-
const name = toKotlinName(key);
|
|
1345
|
-
const numValue = parseFloat(value);
|
|
1346
|
-
if (!isNaN(numValue)) {
|
|
1347
|
-
lines.push(` val ${name} = ${numValue}.dp`);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
lines.push(" }");
|
|
1351
|
-
lines.push("");
|
|
1352
|
-
lines.push(" object Typography {");
|
|
1353
|
-
for (const [key, value] of typography) {
|
|
1354
|
-
const name = toKotlinName(key);
|
|
1355
|
-
if (key.includes("size")) {
|
|
1356
|
-
const numValue = parseFloat(value);
|
|
1357
|
-
if (!isNaN(numValue)) {
|
|
1358
|
-
lines.push(` val ${name} = ${numValue}.sp`);
|
|
1359
|
-
}
|
|
1360
|
-
} else {
|
|
1361
|
-
lines.push(` const val ${name} = "${value}"`);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
lines.push(" }");
|
|
1365
|
-
lines.push("");
|
|
1366
|
-
lines.push(" object Other {");
|
|
1367
|
-
for (const [key, value] of other) {
|
|
1368
|
-
const name = toKotlinName(key);
|
|
1369
|
-
if (isColorValue(value)) {
|
|
1370
|
-
lines.push(` val ${name} = ${hexToKotlinColor(value)}`);
|
|
1371
|
-
} else {
|
|
1372
|
-
const numValue = parseFloat(value);
|
|
1373
|
-
if (!isNaN(numValue)) {
|
|
1374
|
-
lines.push(` val ${name} = ${numValue}.dp`);
|
|
1375
|
-
} else {
|
|
1376
|
-
lines.push(` const val ${name} = "${value}"`);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
lines.push(" }");
|
|
1381
|
-
lines.push("}");
|
|
1382
|
-
lines.push("");
|
|
1383
|
-
return lines.join("\n");
|
|
1384
|
-
}
|
|
1385
|
-
function generateDartOutput(cssVariables, darkModeColors) {
|
|
1386
|
-
const lines = [
|
|
1387
|
-
"// Atomix Design System Tokens",
|
|
1388
|
-
"// Auto-generated - do not edit manually",
|
|
1389
|
-
`// Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1390
|
-
"",
|
|
1391
|
-
"import 'package:flutter/material.dart';",
|
|
1392
|
-
"",
|
|
1393
|
-
"class DesignTokens {",
|
|
1394
|
-
" DesignTokens._();",
|
|
1395
|
-
"",
|
|
1396
|
-
" // Colors (Light Mode)"
|
|
1397
|
-
];
|
|
1398
|
-
const colors = [];
|
|
1399
|
-
const spacing = [];
|
|
1400
|
-
const typography = [];
|
|
1401
|
-
const other = [];
|
|
1402
|
-
for (const [key, value] of Object.entries(cssVariables)) {
|
|
1403
|
-
if (key.includes("-color-")) colors.push([key, value]);
|
|
1404
|
-
else if (key.includes("-spacing-")) spacing.push([key, value]);
|
|
1405
|
-
else if (key.includes("-typography-") || key.includes("-font-")) typography.push([key, value]);
|
|
1406
|
-
else other.push([key, value]);
|
|
1407
|
-
}
|
|
1408
|
-
for (const [key, value] of colors) {
|
|
1409
|
-
const name = toDartName(key);
|
|
1410
|
-
if (isColorValue(value)) {
|
|
1411
|
-
lines.push(` static const ${name} = ${hexToDartColor(value)};`);
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
if (darkModeColors && Object.keys(darkModeColors).length > 0) {
|
|
1415
|
-
lines.push("");
|
|
1416
|
-
lines.push(" // Colors (Dark Mode)");
|
|
1417
|
-
for (const [key, value] of Object.entries(darkModeColors)) {
|
|
1418
|
-
const name = `color${key.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("")}Dark`;
|
|
1419
|
-
if (isColorValue(value)) {
|
|
1420
|
-
lines.push(` static const ${name} = ${hexToDartColor(value)};`);
|
|
1421
|
-
}
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
lines.push("");
|
|
1425
|
-
lines.push(" // Spacing");
|
|
1426
|
-
for (const [key, value] of spacing) {
|
|
1427
|
-
const name = toDartName(key);
|
|
1428
|
-
const numValue = parseFloat(value);
|
|
1429
|
-
if (!isNaN(numValue)) {
|
|
1430
|
-
lines.push(` static const double ${name} = ${numValue};`);
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
lines.push("");
|
|
1434
|
-
lines.push(" // Typography");
|
|
1435
|
-
for (const [key, value] of typography) {
|
|
1436
|
-
const name = toDartName(key);
|
|
1437
|
-
if (key.includes("size")) {
|
|
1438
|
-
const numValue = parseFloat(value);
|
|
1439
|
-
if (!isNaN(numValue)) {
|
|
1440
|
-
lines.push(` static const double ${name} = ${numValue};`);
|
|
1441
|
-
}
|
|
1442
|
-
} else {
|
|
1443
|
-
lines.push(` static const String ${name} = '${value}';`);
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
lines.push("");
|
|
1447
|
-
lines.push(" // Other");
|
|
1448
|
-
for (const [key, value] of other) {
|
|
1449
|
-
const name = toDartName(key);
|
|
1450
|
-
if (isColorValue(value)) {
|
|
1451
|
-
lines.push(` static const ${name} = ${hexToDartColor(value)};`);
|
|
1452
|
-
} else {
|
|
1453
|
-
const numValue = parseFloat(value);
|
|
1454
|
-
if (!isNaN(numValue)) {
|
|
1455
|
-
lines.push(` static const double ${name} = ${numValue};`);
|
|
1456
|
-
} else {
|
|
1457
|
-
lines.push(` static const String ${name} = '${value}';`);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
lines.push("}");
|
|
1462
|
-
lines.push("");
|
|
1463
|
-
return lines.join("\n");
|
|
1464
|
-
}
|
|
1465
|
-
function diffTokens(oldContent, newCssVars, format, newDarkVars) {
|
|
1466
|
-
const added = [];
|
|
1467
|
-
const modified = [];
|
|
1468
|
-
const removed = [];
|
|
1469
|
-
const addedDark = [];
|
|
1470
|
-
const modifiedDark = [];
|
|
1471
|
-
const removedDark = [];
|
|
1472
|
-
if (format === "css" || format === "scss" || format === "less") {
|
|
1473
|
-
const rootMatch = oldContent.match(/:root\s*\{([^}]*)\}/);
|
|
1474
|
-
const darkMatch = oldContent.match(/\.dark[^{]*\{([^}]*)\}/);
|
|
1475
|
-
const oldLightVars = {};
|
|
1476
|
-
if (rootMatch) {
|
|
1477
|
-
const rootContent = rootMatch[1];
|
|
1478
|
-
const varRegex = /(--[\w-]+):\s*([^;]+);/g;
|
|
1479
|
-
let match;
|
|
1480
|
-
while ((match = varRegex.exec(rootContent)) !== null) {
|
|
1481
|
-
oldLightVars[match[1]] = match[2].trim();
|
|
1482
|
-
}
|
|
1483
|
-
} else {
|
|
1484
|
-
const varRegex = /(--[\w-]+):\s*([^;]+);/g;
|
|
1485
|
-
let match;
|
|
1486
|
-
while ((match = varRegex.exec(oldContent)) !== null) {
|
|
1487
|
-
if (!(match[1] in oldLightVars)) {
|
|
1488
|
-
oldLightVars[match[1]] = match[2].trim();
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
const oldDarkVars = {};
|
|
1493
|
-
if (darkMatch) {
|
|
1494
|
-
const darkContent = darkMatch[1];
|
|
1495
|
-
const varRegex = /(--[\w-]+):\s*([^;]+);/g;
|
|
1496
|
-
let match;
|
|
1497
|
-
while ((match = varRegex.exec(darkContent)) !== null) {
|
|
1498
|
-
oldDarkVars[match[1]] = match[2].trim();
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
for (const [key, value] of Object.entries(newCssVars)) {
|
|
1502
|
-
if (!(key in oldLightVars)) {
|
|
1503
|
-
added.push(key);
|
|
1504
|
-
} else if (oldLightVars[key] !== value) {
|
|
1505
|
-
modified.push({ key, old: oldLightVars[key], new: value });
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
for (const key of Object.keys(oldLightVars)) {
|
|
1509
|
-
if (!(key in newCssVars)) {
|
|
1510
|
-
removed.push(key);
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
if (newDarkVars && Object.keys(newDarkVars).length > 0) {
|
|
1514
|
-
const firstKey = Object.keys(newCssVars)[0] || "--atmx-";
|
|
1515
|
-
const prefixMatch = firstKey.match(/^(--[a-z]+-)/);
|
|
1516
|
-
const prefix = prefixMatch ? prefixMatch[1] : "--atmx-";
|
|
1517
|
-
for (const [key, value] of Object.entries(newDarkVars)) {
|
|
1518
|
-
const cssVarName = `${prefix}color-${key}`;
|
|
1519
|
-
if (!(cssVarName in oldDarkVars)) {
|
|
1520
|
-
addedDark.push(cssVarName);
|
|
1521
|
-
} else if (oldDarkVars[cssVarName] !== value) {
|
|
1522
|
-
modifiedDark.push({ key: cssVarName, old: oldDarkVars[cssVarName], new: value });
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
for (const key of Object.keys(oldDarkVars)) {
|
|
1526
|
-
const shortKey = key.replace(new RegExp(`^${prefix}color-`), "");
|
|
1527
|
-
if (!(shortKey in newDarkVars)) {
|
|
1528
|
-
removedDark.push(key);
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return { added, modified, removed, addedDark, modifiedDark, removedDark };
|
|
1534
|
-
}
|
|
1535
|
-
async function promptConfirm(message) {
|
|
1536
|
-
const rl = readline.createInterface({
|
|
1537
|
-
input: process.stdin,
|
|
1538
|
-
output: process.stdout
|
|
1539
|
-
});
|
|
1540
|
-
return new Promise((resolve2) => {
|
|
1541
|
-
rl.question(`${message} [Y/n] `, (answer) => {
|
|
1542
|
-
rl.close();
|
|
1543
|
-
const normalized = answer.toLowerCase().trim();
|
|
1544
|
-
resolve2(normalized === "" || normalized === "y" || normalized === "yes");
|
|
1545
|
-
});
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
async function runSync() {
|
|
1549
|
-
console.log("");
|
|
1550
|
-
console.log(" \u2198\u2198\u2198 Atomix Token Sync");
|
|
1551
|
-
console.log("");
|
|
1552
|
-
const config = findConfig();
|
|
1553
|
-
const effectiveDsId = cliArgs.dsId || config?.dsId;
|
|
1554
|
-
const effectiveApiKey = cliArgs.apiKey || config?.apiKey;
|
|
1555
|
-
const effectiveApiBase = cliArgs.apiBase || config?.apiBase || "https://atomixstudio.eu";
|
|
1556
|
-
const effectiveOutput = cliArgs.output || config?.output || "./tokens.css";
|
|
1557
|
-
const effectiveFormat = cliArgs.format || config?.format || "css";
|
|
1558
|
-
if (!effectiveDsId) {
|
|
1559
|
-
console.error(" Error: Missing design system ID");
|
|
1560
|
-
console.error("");
|
|
1561
|
-
console.error(" Either:");
|
|
1562
|
-
console.error(" 1. Run: npx atomix sync --ds-id <your-ds-id>");
|
|
1563
|
-
console.error(' 2. Create .atomixrc with { "dsId": "<your-ds-id>" }');
|
|
1564
|
-
console.error(" 3. Run: npx atomix init");
|
|
1565
|
-
console.error("");
|
|
1566
|
-
process.exit(1);
|
|
1567
|
-
}
|
|
1568
|
-
console.log(` Fetching tokens from ${effectiveApiBase}...`);
|
|
1569
|
-
const url = `${effectiveApiBase}/api/ds/${effectiveDsId}/tokens?format=export`;
|
|
1570
|
-
const headers = { "Content-Type": "application/json" };
|
|
1571
|
-
if (effectiveApiKey) headers["x-api-key"] = effectiveApiKey;
|
|
1572
|
-
let data;
|
|
1573
|
-
try {
|
|
1574
|
-
const response = await fetch(url, { headers });
|
|
1575
|
-
if (!response.ok) {
|
|
1576
|
-
const errorText = await response.text();
|
|
1577
|
-
console.error(` Error: Failed to fetch tokens (${response.status})`);
|
|
1578
|
-
console.error(` ${errorText}`);
|
|
1579
|
-
process.exit(1);
|
|
1580
|
-
}
|
|
1581
|
-
data = await response.json();
|
|
1582
|
-
} catch (error) {
|
|
1583
|
-
console.error(` Error: Could not connect to ${effectiveApiBase}`);
|
|
1584
|
-
console.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
1585
|
-
process.exit(1);
|
|
1586
|
-
}
|
|
1587
|
-
console.log(` Design System: ${data.meta.name} (v${data.meta.version})`);
|
|
1588
|
-
console.log("");
|
|
1589
|
-
const darkModeColors = data.tokens?.colors?.modes;
|
|
1590
|
-
let newContent;
|
|
1591
|
-
switch (effectiveFormat) {
|
|
1592
|
-
case "css":
|
|
1593
|
-
newContent = generateCSSOutput(data.cssVariables, darkModeColors?.dark);
|
|
1594
|
-
break;
|
|
1595
|
-
case "scss":
|
|
1596
|
-
newContent = generateSCSSOutput(data.cssVariables, darkModeColors?.dark);
|
|
1597
|
-
break;
|
|
1598
|
-
case "less":
|
|
1599
|
-
newContent = generateLessOutput(data.cssVariables, darkModeColors?.dark);
|
|
1600
|
-
break;
|
|
1601
|
-
case "json":
|
|
1602
|
-
newContent = generateJSONOutput(data.tokens);
|
|
1603
|
-
break;
|
|
1604
|
-
case "js":
|
|
1605
|
-
newContent = generateJSOutput(data.tokens);
|
|
1606
|
-
break;
|
|
1607
|
-
case "ts":
|
|
1608
|
-
newContent = generateTSOutput(data.tokens);
|
|
1609
|
-
break;
|
|
1610
|
-
case "swift":
|
|
1611
|
-
newContent = generateSwiftOutput(data.cssVariables, darkModeColors?.dark);
|
|
1612
|
-
break;
|
|
1613
|
-
case "kotlin":
|
|
1614
|
-
newContent = generateKotlinOutput(data.cssVariables, darkModeColors?.dark);
|
|
1615
|
-
break;
|
|
1616
|
-
case "dart":
|
|
1617
|
-
newContent = generateDartOutput(data.cssVariables, darkModeColors?.dark);
|
|
1618
|
-
break;
|
|
1619
|
-
default:
|
|
1620
|
-
newContent = generateCSSOutput(data.cssVariables, darkModeColors?.dark);
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
const outputPath = path.resolve(process.cwd(), effectiveOutput);
|
|
1624
|
-
const fileExists = fs.existsSync(outputPath);
|
|
1625
|
-
const supportsDiff = ["css", "scss", "less"].includes(effectiveFormat);
|
|
1626
|
-
if (fileExists && supportsDiff) {
|
|
1627
|
-
const oldContent = fs.readFileSync(outputPath, "utf-8");
|
|
1628
|
-
const diff = diffTokens(oldContent, data.cssVariables, effectiveFormat, darkModeColors?.dark);
|
|
1629
|
-
const lightChanges = diff.added.length + diff.modified.length + diff.removed.length;
|
|
1630
|
-
const darkChanges = diff.addedDark.length + diff.modifiedDark.length + diff.removedDark.length;
|
|
1631
|
-
const totalChanges = lightChanges + darkChanges;
|
|
1632
|
-
if (totalChanges === 0) {
|
|
1633
|
-
console.log(" \u2713 Already up to date!");
|
|
1634
|
-
console.log("");
|
|
1635
|
-
process.exit(0);
|
|
1636
|
-
}
|
|
1637
|
-
console.log(` Changes detected in ${path.basename(effectiveOutput)}:`);
|
|
1638
|
-
console.log("");
|
|
1639
|
-
if (lightChanges > 0) {
|
|
1640
|
-
console.log(" Light Mode:");
|
|
1641
|
-
if (diff.modified.length > 0) {
|
|
1642
|
-
for (const { key, old: oldVal, new: newVal } of diff.modified) {
|
|
1643
|
-
console.log(` ${key}`);
|
|
1644
|
-
console.log(` - ${oldVal}`);
|
|
1645
|
-
console.log(` + ${newVal}`);
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
if (diff.added.length > 0) {
|
|
1649
|
-
console.log(` + ${diff.added.length} new token(s)`);
|
|
1650
|
-
}
|
|
1651
|
-
if (diff.removed.length > 0) {
|
|
1652
|
-
console.log(` - ${diff.removed.length} removed token(s)`);
|
|
1653
|
-
}
|
|
1654
|
-
console.log("");
|
|
1655
|
-
}
|
|
1656
|
-
if (darkChanges > 0) {
|
|
1657
|
-
console.log(" Dark Mode:");
|
|
1658
|
-
if (diff.modifiedDark.length > 0) {
|
|
1659
|
-
for (const { key, old: oldVal, new: newVal } of diff.modifiedDark) {
|
|
1660
|
-
console.log(` ${key}`);
|
|
1661
|
-
console.log(` - ${oldVal}`);
|
|
1662
|
-
console.log(` + ${newVal}`);
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
if (diff.addedDark.length > 0) {
|
|
1666
|
-
console.log(` + ${diff.addedDark.length} new token(s)`);
|
|
1667
|
-
}
|
|
1668
|
-
if (diff.removedDark.length > 0) {
|
|
1669
|
-
console.log(` - ${diff.removedDark.length} removed token(s)`);
|
|
1670
|
-
}
|
|
1671
|
-
console.log("");
|
|
1672
|
-
}
|
|
1673
|
-
console.log(` Total: ${totalChanges} change(s) (Light: ${lightChanges}, Dark: ${darkChanges})`);
|
|
1674
|
-
console.log("");
|
|
1675
|
-
if (!cliArgs.yes) {
|
|
1676
|
-
const confirmed = await promptConfirm(" Apply changes?");
|
|
1677
|
-
if (!confirmed) {
|
|
1678
|
-
console.log(" Cancelled.");
|
|
1679
|
-
process.exit(0);
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
} else if (!fileExists) {
|
|
1683
|
-
console.log(` Creating ${effectiveOutput}...`);
|
|
1684
|
-
console.log(` ${Object.keys(data.cssVariables).length} tokens`);
|
|
1685
|
-
console.log("");
|
|
1686
|
-
if (!cliArgs.yes) {
|
|
1687
|
-
const confirmed = await promptConfirm(" Create file?");
|
|
1688
|
-
if (!confirmed) {
|
|
1689
|
-
console.log(" Cancelled.");
|
|
1690
|
-
process.exit(0);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
const outputDir = path.dirname(outputPath);
|
|
1695
|
-
if (!fs.existsSync(outputDir)) {
|
|
1696
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
1697
|
-
}
|
|
1698
|
-
fs.writeFileSync(outputPath, newContent);
|
|
1699
|
-
console.log("");
|
|
1700
|
-
console.log(` \u2713 Tokens synced \u2192 ${effectiveOutput}`);
|
|
1701
|
-
if (cliArgs.rules) {
|
|
1702
|
-
console.log("");
|
|
1703
|
-
console.log(" Syncing AI guidance rules...");
|
|
1704
|
-
const rulesDir = cliArgs.rulesDir || process.cwd();
|
|
1705
|
-
const rulesDirResolved = path.resolve(process.cwd(), rulesDir);
|
|
1706
|
-
const toolsToSync = [
|
|
1707
|
-
{ tool: "cursor", filename: ".cursorrules" },
|
|
1708
|
-
{ tool: "windsurf", filename: ".windsurfrules" },
|
|
1709
|
-
{ tool: "cline", filename: ".clinerules" },
|
|
1710
|
-
{ tool: "continue", filename: ".continuerules" },
|
|
1711
|
-
{ tool: "copilot", filename: "copilot-instructions.md", dir: ".github" },
|
|
1712
|
-
{ tool: "generic", filename: "AI_GUIDELINES.md" }
|
|
1713
|
-
];
|
|
1714
|
-
const existingTools = toolsToSync.filter((t) => {
|
|
1715
|
-
const filePath = t.dir ? path.join(rulesDirResolved, t.dir, t.filename) : path.join(rulesDirResolved, t.filename);
|
|
1716
|
-
return fs.existsSync(filePath);
|
|
1717
|
-
});
|
|
1718
|
-
const toolsToWrite = existingTools.length > 0 ? existingTools : [{ tool: "cursor", filename: ".cursorrules" }];
|
|
1719
|
-
for (const { tool, filename, dir } of toolsToWrite) {
|
|
1720
|
-
try {
|
|
1721
|
-
const rulesUrl = `${effectiveApiBase}/api/ds/${effectiveDsId}/rules?format=${tool}`;
|
|
1722
|
-
const headers2 = { "Content-Type": "application/json" };
|
|
1723
|
-
if (effectiveApiKey) headers2["x-api-key"] = effectiveApiKey;
|
|
1724
|
-
const response = await fetch(rulesUrl, { headers: headers2 });
|
|
1725
|
-
if (!response.ok) {
|
|
1726
|
-
console.error(` \u2717 Failed to fetch ${tool} rules: ${response.status}`);
|
|
1727
|
-
continue;
|
|
1728
|
-
}
|
|
1729
|
-
const rulesData = await response.json();
|
|
1730
|
-
if (!rulesData.content) {
|
|
1731
|
-
console.error(` \u2717 No content for ${tool} rules`);
|
|
1732
|
-
continue;
|
|
1733
|
-
}
|
|
1734
|
-
const targetDir = dir ? path.join(rulesDirResolved, dir) : rulesDirResolved;
|
|
1735
|
-
if (!fs.existsSync(targetDir)) {
|
|
1736
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1737
|
-
}
|
|
1738
|
-
const filePath = path.join(targetDir, filename);
|
|
1739
|
-
fs.writeFileSync(filePath, rulesData.content);
|
|
1740
|
-
const relativePath = dir ? `${dir}/${filename}` : filename;
|
|
1741
|
-
console.log(` \u2713 ${relativePath}`);
|
|
1742
|
-
} catch (error) {
|
|
1743
|
-
console.error(` \u2717 Error syncing ${tool} rules:`, error instanceof Error ? error.message : String(error));
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
console.log("");
|
|
1748
|
-
}
|
|
1749
|
-
async function runInit() {
|
|
1750
|
-
console.log("");
|
|
1751
|
-
console.log(" \u2198\u2198\u2198 Atomix Config Setup");
|
|
1752
|
-
console.log("");
|
|
1753
|
-
const rl = readline.createInterface({
|
|
1754
|
-
input: process.stdin,
|
|
1755
|
-
output: process.stdout
|
|
1756
|
-
});
|
|
1757
|
-
const question = (prompt) => new Promise((resolve2) => rl.question(prompt, resolve2));
|
|
1758
|
-
const dsId2 = cliArgs.dsId || await question(" Design System ID: ");
|
|
1759
|
-
const output = await question(" Output file [./tokens.css]: ") || "./tokens.css";
|
|
1760
|
-
const format = await question(" Format (css/json/ts) [css]: ") || "css";
|
|
1761
|
-
rl.close();
|
|
1762
|
-
const config = {
|
|
1763
|
-
dsId: dsId2,
|
|
1764
|
-
output,
|
|
1765
|
-
format
|
|
1766
|
-
};
|
|
1767
|
-
const configPath = path.join(process.cwd(), ".atomixrc");
|
|
1768
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1769
|
-
console.log("");
|
|
1770
|
-
console.log(` \u2713 Created .atomixrc`);
|
|
1771
|
-
console.log("");
|
|
1772
|
-
console.log(" Now run: npx atomix sync");
|
|
1773
|
-
console.log("");
|
|
1774
|
-
}
|
|
1775
|
-
function showHelp() {
|
|
1776
|
-
console.log(`
|
|
1777
|
-
\u2198\u2198\u2198 Atomix CLI
|
|
1778
|
-
|
|
1779
|
-
COMMANDS
|
|
1780
|
-
sync Sync design tokens to your project
|
|
1781
|
-
init Create .atomixrc config file
|
|
1782
|
-
help Show this help message
|
|
1783
|
-
(none) Start MCP server for AI tools
|
|
1784
|
-
|
|
1785
|
-
SYNC OPTIONS
|
|
1786
|
-
--ds-id Design system ID (or set in .atomixrc)
|
|
1787
|
-
--api-key API key for private design systems
|
|
1788
|
-
--output, -o Output file path [./tokens.css]
|
|
1789
|
-
--format Output format [css]
|
|
1790
|
-
WEB:
|
|
1791
|
-
css - CSS custom properties (:root { --var: value })
|
|
1792
|
-
scss - CSS vars + SCSS variables ($var: value)
|
|
1793
|
-
less - CSS vars + Less variables (@var: value)
|
|
1794
|
-
json - Raw token JSON
|
|
1795
|
-
ts - TypeScript with types
|
|
1796
|
-
js - JavaScript ES module
|
|
1797
|
-
NATIVE:
|
|
1798
|
-
swift - SwiftUI (iOS/macOS)
|
|
1799
|
-
kotlin - Jetpack Compose (Android)
|
|
1800
|
-
dart - Flutter
|
|
1801
|
-
--no-rules Skip syncing AI rules files (tokens + rules sync by default)
|
|
1802
|
-
--rules-dir Directory for rules files [project root]
|
|
1803
|
-
--exclude Glob pattern to exclude (can use multiple times)
|
|
1804
|
-
-y, --yes Auto-confirm changes
|
|
1805
|
-
|
|
1806
|
-
MCP SERVER OPTIONS
|
|
1807
|
-
--ds-id Design system ID (required)
|
|
1808
|
-
--api-key API key for private design systems
|
|
1809
|
-
--api-base API base URL [https://atomixstudio.eu]
|
|
1810
|
-
|
|
1811
|
-
EXAMPLES
|
|
1812
|
-
npx heyatomix sync # Sync tokens + AI rules (default)
|
|
1813
|
-
npx heyatomix sync --no-rules # Sync tokens only
|
|
1814
|
-
npx heyatomix sync -o ./src/tokens.css
|
|
1815
|
-
npx heyatomix sync --format scss -o ./src/styles/_tokens.scss
|
|
1816
|
-
npx heyatomix sync --format swift -o ./Sources/DesignTokens.swift
|
|
1817
|
-
npx heyatomix init
|
|
1818
|
-
npx heyatomix --ds-id abc123 # Start MCP server
|
|
1819
|
-
|
|
1820
|
-
CONFIG FILE (.atomixrc)
|
|
1821
|
-
{
|
|
1822
|
-
"dsId": "your-design-system-id",
|
|
1823
|
-
"output": "./src/styles/tokens.css",
|
|
1824
|
-
"format": "css",
|
|
1825
|
-
"exclude": ["legacy/**", "vendor/**"]
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
DEFAULT SCANNED FILES
|
|
1829
|
-
*.tsx, *.jsx, *.ts, *.js, *.css, *.scss, *.less, *.vue, *.svelte
|
|
1830
|
-
|
|
1831
|
-
DEFAULT EXCLUDED
|
|
1832
|
-
node_modules/**, dist/**, build/**, .next/**, *.min.*, *.d.ts
|
|
1833
|
-
`);
|
|
1834
|
-
}
|
|
1835
1650
|
async function startServer() {
|
|
1836
1651
|
if (!dsId) {
|
|
1837
1652
|
console.error("Error: Missing --ds-id argument");
|
|
1838
|
-
console.error("Usage: npx atomix --ds-id <id> --api-key <key>");
|
|
1653
|
+
console.error("Usage: npx atomix --ds-id <id> [--api-key <key>]");
|
|
1654
|
+
console.error("Note: --api-key is optional (only needed for private design systems)");
|
|
1839
1655
|
console.error("");
|
|
1840
1656
|
console.error("For sync command: npx atomix sync --help");
|
|
1841
1657
|
console.error("Get your DS ID from https://atomixstudio.eu/ds/[your-ds-id]");
|
|
@@ -1846,21 +1662,7 @@ async function startServer() {
|
|
|
1846
1662
|
console.error(`Atomix MCP Server started for design system: ${dsId}`);
|
|
1847
1663
|
}
|
|
1848
1664
|
async function main() {
|
|
1849
|
-
|
|
1850
|
-
case "sync":
|
|
1851
|
-
await runSync();
|
|
1852
|
-
break;
|
|
1853
|
-
case "init":
|
|
1854
|
-
await runInit();
|
|
1855
|
-
break;
|
|
1856
|
-
case "help":
|
|
1857
|
-
showHelp();
|
|
1858
|
-
break;
|
|
1859
|
-
case "server":
|
|
1860
|
-
default:
|
|
1861
|
-
await startServer();
|
|
1862
|
-
break;
|
|
1863
|
-
}
|
|
1665
|
+
await startServer();
|
|
1864
1666
|
}
|
|
1865
1667
|
main().catch((error) => {
|
|
1866
1668
|
console.error("Failed:", error);
|