@atomixstudio/mcp 0.1.1 → 1.0.0

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.
@@ -1,974 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Sync Component Tokens from Atomix Lab
4
- *
5
- * FUTURE-PROOF DYNAMIC VERSION
6
- *
7
- * This script reads component-defaults.ts and COMPONENT_MANIFEST from atomix-lab
8
- * and generates component-tokens.ts for the MCP server.
9
- *
10
- * Features:
11
- * - Auto-discovers components from COMPONENT_DEFAULTS
12
- * - Extracts variants/sizes dynamically (no hardcoding)
13
- * - Uses token-map.json for semantic key → MCP path transformation
14
- * - Validates all token references
15
- * - Supports multi-tenant via ATOMIX_TENANT_ID env var
16
- *
17
- * Usage:
18
- * node scripts/sync-component-tokens.cjs
19
- * ATOMIX_TENANT_ID=acme-corp node scripts/sync-component-tokens.cjs
20
- *
21
- * Called automatically by:
22
- * - npm run sync (in atomix-mcp)
23
- * - npm run build (in atomix package)
24
- */
25
-
26
- const fs = require("fs");
27
- const path = require("path");
28
-
29
- // ============================================
30
- // CONFIGURATION
31
- // ============================================
32
-
33
- const CONFIG = {
34
- tenantId: process.env.ATOMIX_TENANT_ID || "default",
35
- cloudApiUrl: process.env.ATOMIX_CLOUD_API || null,
36
- };
37
-
38
- // Paths
39
- const LAB_DEFAULTS_PATH = path.join(__dirname, "../../../apps/atomix-lab/src/config/component-defaults.ts");
40
- const LAB_TENANT_OVERRIDES_PATH = path.join(__dirname, "../../../apps/atomix-lab/src/config/tenants");
41
- const TOKEN_MAP_PATH = path.join(__dirname, "../../atomix/dist/token-map.json");
42
- const MCP_TOKENS_PATH = path.join(__dirname, "../src/component-tokens.ts");
43
- const MCP_TENANT_DATA_PATH = path.join(__dirname, "../data/tenants");
44
- const SNAPSHOT_PATH = path.join(__dirname, "../data/component-tokens-snapshot.json");
45
-
46
- // ============================================
47
- // TOKEN MAP LOADING
48
- // ============================================
49
-
50
- let tokenMap = null;
51
-
52
- function loadTokenMap() {
53
- if (tokenMap) return tokenMap;
54
-
55
- try {
56
- if (fs.existsSync(TOKEN_MAP_PATH)) {
57
- tokenMap = JSON.parse(fs.readFileSync(TOKEN_MAP_PATH, "utf-8"));
58
- console.log(` Loaded token-map.json (${tokenMap.tokenCount} tokens)`);
59
- } else {
60
- console.warn(" Warning: token-map.json not found. Run 'npm run build' in packages/atomix first.");
61
- tokenMap = { tokens: {}, semanticKeys: {} };
62
- }
63
- } catch (error) {
64
- console.warn(" Warning: Failed to parse token-map.json:", error.message);
65
- tokenMap = { tokens: {}, semanticKeys: {} };
66
- }
67
-
68
- return tokenMap;
69
- }
70
-
71
- // ============================================
72
- // SEMANTIC KEY TRANSFORMER
73
- // ============================================
74
-
75
- /**
76
- * Transforms semantic keys (like "action-primary", "bg-surface") to MCP paths.
77
- * Uses token-map.json for validation and path lookup.
78
- */
79
- function semanticToMCPPath(semanticKey) {
80
- const map = loadTokenMap();
81
-
82
- // Direct mappings for common semantic keys
83
- const directMappings = {
84
- // Action colors
85
- "action-primary": "colors.modes.{mode}.actionPrimary",
86
- "action-primary-hover": "colors.modes.{mode}.actionPrimaryHover",
87
- "action-secondary": "colors.modes.{mode}.actionSecondary",
88
- "action-secondary-hover": "colors.modes.{mode}.actionSecondaryHover",
89
- "action-destructive": "colors.modes.{mode}.actionDestructive",
90
- "action-destructive-hover": "colors.modes.{mode}.actionDestructiveHover",
91
-
92
- // Backgrounds
93
- "bg-page": "colors.modes.{mode}.bgPage",
94
- "bg-surface": "colors.modes.{mode}.bgSurface",
95
- "bg-muted": "colors.modes.{mode}.bgMuted",
96
- "bg-subtle": "colors.modes.{mode}.bgSubtle",
97
- "bg-elevated": "colors.modes.{mode}.bgElevated",
98
-
99
- // Text
100
- "text-primary": "colors.modes.{mode}.textPrimary",
101
- "text-secondary": "colors.modes.{mode}.textSecondary",
102
- "text-muted": "colors.modes.{mode}.textMuted",
103
- "text-disabled": "colors.modes.{mode}.textDisabled",
104
- "text-on-brand": "colors.modes.{mode}.textOnBrand",
105
-
106
- // Borders
107
- "border-primary": "colors.modes.{mode}.borderPrimary",
108
- "border-secondary": "colors.modes.{mode}.borderSecondary",
109
- "border-strong": "colors.modes.{mode}.borderStrong",
110
- "border-transparent": "transparent",
111
- "border-error": "colors.adaptive.error.{mode}.border",
112
- "border-success": "colors.adaptive.success.{mode}.border",
113
-
114
- // On colors (for indicators)
115
- "on-action-primary": "colors.static.white",
116
- "on-action-secondary": "colors.modes.{mode}.textPrimary",
117
- "on-action-destructive": "colors.static.white",
118
- "on-bg-primary": "colors.modes.{mode}.onBgPrimary",
119
- "on-bg-secondary": "colors.modes.{mode}.onBgSecondary",
120
-
121
- // Brand
122
- "brand-primary": "colors.static.brand.primary",
123
- "text-brand": "colors.static.brand.primary",
124
- "text-brand-secondary": "colors.static.brandSecondary.primary",
125
-
126
- // Feedback text
127
- "text-error": "colors.adaptive.error.{mode}.text",
128
- "text-success": "colors.adaptive.success.{mode}.text",
129
- "text-warning": "colors.adaptive.warning.{mode}.text",
130
- "text-info": "colors.adaptive.info.{mode}.text",
131
-
132
- // Special
133
- "transparent": "transparent",
134
- "none": "none",
135
- };
136
-
137
- // Check direct mapping first
138
- if (directMappings[semanticKey]) {
139
- return directMappings[semanticKey];
140
- }
141
-
142
- // Check if it's in the token map's semantic keys
143
- if (map.semanticKeys && map.semanticKeys[semanticKey]) {
144
- return map.semanticKeys[semanticKey].mcpPath;
145
- }
146
-
147
- // Try to construct from key pattern
148
- const kebabToCamel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
149
-
150
- // Check if it matches a mode color pattern
151
- const camelKey = kebabToCamel(semanticKey);
152
- const lightPath = `colors.modes.light.${camelKey}`;
153
- if (map.tokens && map.tokens[lightPath]) {
154
- return `colors.modes.{mode}.${camelKey}`;
155
- }
156
-
157
- // Return as-is (might be a non-color token like spacing key)
158
- return semanticKey;
159
- }
160
-
161
- // ============================================
162
- // DYNAMIC COMPONENT PARSING
163
- // ============================================
164
-
165
- /**
166
- * Auto-discover components from COMPONENT_DEFAULTS export
167
- */
168
- function discoverComponents(content) {
169
- const components = [];
170
-
171
- // Find COMPONENT_DEFAULTS export
172
- const defaultsMatch = content.match(
173
- /export const COMPONENT_DEFAULTS\s*=\s*\{([\s\S]*?)\}\s*as const/
174
- );
175
-
176
- if (!defaultsMatch) {
177
- console.error(" Error: Could not find COMPONENT_DEFAULTS export");
178
- return components;
179
- }
180
-
181
- // Extract component names dynamically
182
- const componentRegex = /(\w+):\s*(\w+)_DEFAULTS/g;
183
- let match;
184
- while ((match = componentRegex.exec(defaultsMatch[1])) !== null) {
185
- components.push({
186
- key: match[1], // e.g., "button"
187
- defaultsName: match[2], // e.g., "BUTTON"
188
- });
189
- }
190
-
191
- console.log(` Found ${components.length} components: ${components.map(c => c.key).join(", ")}`);
192
-
193
- return components;
194
- }
195
-
196
- /**
197
- * Parse COMPONENT_MANIFEST for metadata
198
- */
199
- function parseManifest(content) {
200
- const manifest = {};
201
-
202
- // Find COMPONENT_MANIFEST export
203
- const manifestMatch = content.match(
204
- /export const COMPONENT_MANIFEST[^=]*=\s*\{([\s\S]*?)\n\};/
205
- );
206
-
207
- if (!manifestMatch) {
208
- console.log(" No COMPONENT_MANIFEST found, using auto-generated metadata");
209
- return manifest;
210
- }
211
-
212
- // Parse each component entry
213
- const entryRegex = /(\w+):\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
214
- let match;
215
- while ((match = entryRegex.exec(manifestMatch[1])) !== null) {
216
- const key = match[1];
217
- const body = match[2];
218
-
219
- // Extract fields
220
- const description = body.match(/description:\s*"([^"]+)"/)?.[1] || `${key} component`;
221
- const mcpName = body.match(/mcpName:\s*"([^"]+)"/)?.[1] || key;
222
- const tenantOverridable = body.includes("tenantOverridable: true");
223
-
224
- // Extract arrays
225
- const lockedTokens = extractArray(body, "lockedTokens");
226
- const requiredVariants = extractArray(body, "requiredVariants");
227
-
228
- manifest[key] = {
229
- description,
230
- mcpName,
231
- tenantOverridable,
232
- lockedTokens,
233
- requiredVariants,
234
- };
235
- }
236
-
237
- console.log(` Parsed manifest for ${Object.keys(manifest).length} components`);
238
-
239
- return manifest;
240
- }
241
-
242
- function extractArray(text, propName) {
243
- const match = text.match(new RegExp(`${propName}:\\s*\\[([^\\]]*)]`));
244
- if (!match) return [];
245
-
246
- const items = [];
247
- const itemRegex = /"([^"]+)"/g;
248
- let itemMatch;
249
- while ((itemMatch = itemRegex.exec(match[1])) !== null) {
250
- items.push(itemMatch[1]);
251
- }
252
- return items;
253
- }
254
-
255
- /**
256
- * Extract the raw defaults block for a component
257
- */
258
- function extractDefaultsBlock(content, defaultsName) {
259
- const regex = new RegExp(
260
- `export const ${defaultsName}_DEFAULTS[^=]*=\\s*(\\{[\\s\\S]*?\\n\\});`,
261
- "m"
262
- );
263
- const match = content.match(regex);
264
- return match ? match[1] : null;
265
- }
266
-
267
- /**
268
- * Extract object keys from a nested property (variants, sizes, etc.)
269
- */
270
- function extractObjectKeys(block, propName) {
271
- // Find the property block - handle nested braces
272
- const propRegex = new RegExp(`${propName}:\\s*\\{`, "g");
273
- const match = propRegex.exec(block);
274
- if (!match) return [];
275
-
276
- // Find matching closing brace
277
- let depth = 1;
278
- let i = match.index + match[0].length;
279
- const start = i;
280
-
281
- while (i < block.length && depth > 0) {
282
- if (block[i] === "{") depth++;
283
- if (block[i] === "}") depth--;
284
- i++;
285
- }
286
-
287
- const propBody = block.slice(start, i - 1);
288
-
289
- // Extract top-level keys
290
- const keys = [];
291
- const keyRegex = /^\s*["']?([\w-]+)["']?\s*:\s*\{/gm;
292
- let keyMatch;
293
- while ((keyMatch = keyRegex.exec(propBody)) !== null) {
294
- keys.push(keyMatch[1]);
295
- }
296
-
297
- return keys;
298
- }
299
-
300
- /**
301
- * Extract property values from a variant/size block
302
- */
303
- function extractPropertyValues(block, variantKey) {
304
- const props = {};
305
-
306
- // Find the variant/size block
307
- const variantRegex = new RegExp(`["']?${variantKey}["']?:\\s*\\{([\\s\\S]*?)\\n \\}`, "m");
308
- const match = block.match(variantRegex);
309
- if (!match) return props;
310
-
311
- const body = match[1];
312
-
313
- // Extract simple string properties
314
- const stringPropRegex = /(\w+):\s*"([^"]+)"/g;
315
- let propMatch;
316
- while ((propMatch = stringPropRegex.exec(body)) !== null) {
317
- props[propMatch[1]] = propMatch[2];
318
- }
319
-
320
- // Extract boolean properties
321
- const boolPropRegex = /(\w+):\s*(true|false)/g;
322
- while ((propMatch = boolPropRegex.exec(body)) !== null) {
323
- props[propMatch[1]] = propMatch[2] === "true";
324
- }
325
-
326
- return props;
327
- }
328
-
329
- // ============================================
330
- // MCP TOKEN GENERATION
331
- // ============================================
332
-
333
- /**
334
- * Generate MCP component tokens from parsed data
335
- */
336
- function generateMCPTokens(components, manifest, content) {
337
- const output = {};
338
-
339
- for (const comp of components) {
340
- const block = extractDefaultsBlock(content, comp.defaultsName);
341
- if (!block) {
342
- console.warn(` Warning: Could not find ${comp.defaultsName}_DEFAULTS block`);
343
- continue;
344
- }
345
-
346
- const variants = extractObjectKeys(block, "variants");
347
- const sizes = extractObjectKeys(block, "sizes");
348
- const states = extractObjectKeys(block, "states");
349
- const validation = extractObjectKeys(block, "validation");
350
-
351
- const meta = manifest[comp.key] || { description: `${comp.key} component`, mcpName: comp.key };
352
- const mcpKey = meta.mcpName || comp.key;
353
-
354
- // Build variant tokens
355
- const variantTokens = {};
356
- const variantKeys = variants.length > 0 ? variants : (states.length > 0 ? states : validation);
357
-
358
- for (const variantKey of variantKeys) {
359
- const props = extractPropertyValues(block, variantKey);
360
- const tokens = {};
361
-
362
- for (const [propKey, propValue] of Object.entries(props)) {
363
- if (typeof propValue === "string" && !propValue.includes("{")) {
364
- tokens[propKey] = semanticToMCPPath(propValue);
365
- }
366
- }
367
-
368
- if (Object.keys(tokens).length > 0) {
369
- variantTokens[variantKey] = {
370
- description: `${variantKey} variant`,
371
- tokens,
372
- };
373
- }
374
- }
375
-
376
- // Build size tokens
377
- const sizeTokens = {};
378
- for (const sizeKey of sizes) {
379
- const props = extractPropertyValues(block, sizeKey);
380
- const tokens = {};
381
-
382
- for (const [propKey, propValue] of Object.entries(props)) {
383
- if (typeof propValue === "string") {
384
- // Map spacing/radius keys to paths
385
- if (["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl"].includes(propValue)) {
386
- if (propKey.includes("padding") || propKey.includes("gap")) {
387
- tokens[propKey] = `spacing.scale.${propValue}`;
388
- } else if (propKey.includes("Radius") || propKey.includes("borderRadius")) {
389
- tokens[propKey] = `radius.scale.${propValue}`;
390
- } else {
391
- tokens[propKey] = propValue;
392
- }
393
- } else if (propValue.includes("-") && (propValue.startsWith("text-") || propValue.startsWith("title-") || propValue.startsWith("label-"))) {
394
- // TypeSet reference
395
- tokens[propKey] = `typography.typeSets.${propValue}`;
396
- } else {
397
- tokens[propKey] = propValue;
398
- }
399
- }
400
- }
401
-
402
- if (Object.keys(tokens).length > 0) {
403
- sizeTokens[sizeKey] = { tokens };
404
- }
405
- }
406
-
407
- // Build component output
408
- output[mcpKey] = {
409
- description: meta.description,
410
- };
411
-
412
- if (Object.keys(variantTokens).length > 0) {
413
- output[mcpKey].variants = variantTokens;
414
- }
415
-
416
- if (Object.keys(sizeTokens).length > 0) {
417
- output[mcpKey].sizes = sizeTokens;
418
- }
419
-
420
- // Add shared tokens (global properties)
421
- const sharedTokens = {};
422
- const globalProps = ["fontFamily", "titleFontFamily", "bodyFontFamily"];
423
- for (const prop of globalProps) {
424
- const match = block.match(new RegExp(`${prop}:\\s*"([^"]+)"`));
425
- if (match) {
426
- sharedTokens[prop] = `typography.fontFamily.${match[1]}`;
427
- }
428
- }
429
-
430
- if (Object.keys(sharedTokens).length > 0) {
431
- output[mcpKey].shared = sharedTokens;
432
- }
433
- }
434
-
435
- // Handle selectionControls split into checkbox, radio, toggle
436
- if (output.selectionControls) {
437
- const base = output.selectionControls;
438
-
439
- if (base.variants) {
440
- // Create individual control components
441
- for (const controlType of ["checkbox", "radio", "toggle"]) {
442
- if (base.variants[controlType]) {
443
- output[controlType] = {
444
- description: `${controlType.charAt(0).toUpperCase() + controlType.slice(1)} selection control`,
445
- variants: {
446
- default: base.variants[controlType],
447
- },
448
- sizes: base.sizes,
449
- shared: {
450
- transitionDuration: "motion.duration.fast",
451
- focusRing: "shadows.focus.ring",
452
- },
453
- };
454
- }
455
- }
456
- }
457
-
458
- // Keep selectionControls as an alias
459
- }
460
-
461
- return output;
462
- }
463
-
464
- // ============================================
465
- // TENANT OVERRIDES LOADING
466
- // ============================================
467
-
468
- /**
469
- * Load tenant overrides from the Lab's TypeScript files.
470
- * Parses the TENANT_*_OVERRIDES exports from tenants/[id]/overrides.ts
471
- */
472
- function loadTenantOverrides(tenantId) {
473
- const overrides = {
474
- button: { variants: {}, borders: {} },
475
- card: { variants: {} },
476
- input: { variants: {} },
477
- heading: { variants: {} },
478
- };
479
-
480
- const tenantOverridesPath = path.join(LAB_TENANT_OVERRIDES_PATH, tenantId, "overrides.ts");
481
-
482
- if (!fs.existsSync(tenantOverridesPath)) {
483
- console.log(` No tenant overrides found for ${tenantId}`);
484
- return overrides;
485
- }
486
-
487
- try {
488
- const content = fs.readFileSync(tenantOverridesPath, "utf-8");
489
-
490
- // Parse TENANT_BUTTON_OVERRIDES
491
- const buttonMatch = content.match(/TENANT_BUTTON_OVERRIDES[^=]*=\s*\{([\s\S]*?)\n\};/);
492
- if (buttonMatch) {
493
- const variantsMatch = buttonMatch[1].match(/variants:\s*\{([\s\S]*?)\n \}/);
494
- if (variantsMatch) {
495
- const variantKeys = extractVariantKeys(variantsMatch[1]);
496
- for (const key of variantKeys) {
497
- const props = extractPropertyValues(buttonMatch[1], key);
498
- if (Object.keys(props).length > 0) {
499
- overrides.button.variants[key] = props;
500
- }
501
- }
502
- }
503
- }
504
-
505
- // Parse TENANT_CARD_OVERRIDES
506
- const cardMatch = content.match(/TENANT_CARD_OVERRIDES[^=]*=\s*\{([\s\S]*?)\n\};/);
507
- if (cardMatch) {
508
- const variantsMatch = cardMatch[1].match(/variants:\s*\{([\s\S]*?)\n \}/);
509
- if (variantsMatch) {
510
- const variantKeys = extractVariantKeys(variantsMatch[1]);
511
- for (const key of variantKeys) {
512
- const props = extractPropertyValues(cardMatch[1], key);
513
- if (Object.keys(props).length > 0) {
514
- overrides.card.variants[key] = props;
515
- }
516
- }
517
- }
518
- }
519
-
520
- const variantCount = Object.keys(overrides.button.variants).length +
521
- Object.keys(overrides.card.variants).length;
522
- if (variantCount > 0) {
523
- console.log(` Loaded ${variantCount} tenant variant overrides for ${tenantId}`);
524
- }
525
-
526
- return overrides;
527
- } catch (error) {
528
- console.warn(` Warning: Failed to parse tenant overrides for ${tenantId}:`, error.message);
529
- return overrides;
530
- }
531
- }
532
-
533
- /**
534
- * Extract variant keys from a variants block
535
- */
536
- function extractVariantKeys(block) {
537
- const keys = [];
538
- const keyRegex = /^\s*["']?([\w-]+)["']?\s*:\s*\{/gm;
539
- let match;
540
- while ((match = keyRegex.exec(block)) !== null) {
541
- // Skip empty objects or comments
542
- if (match[1] && !match[1].startsWith("//")) {
543
- keys.push(match[1]);
544
- }
545
- }
546
- return keys;
547
- }
548
-
549
- /**
550
- * Merge tenant overrides into component tokens
551
- */
552
- function mergeTenantOverrides(mcpTokens, tenantOverrides) {
553
- // Merge button variants
554
- for (const [variantKey, variantConfig] of Object.entries(tenantOverrides.button.variants)) {
555
- if (!mcpTokens.button) continue;
556
- if (!mcpTokens.button.variants) mcpTokens.button.variants = {};
557
-
558
- mcpTokens.button.variants[variantKey] = {
559
- description: `${variantKey} variant (tenant override)`,
560
- tokens: transformConfigToTokens(variantConfig, "button"),
561
- };
562
- }
563
-
564
- // Merge card variants
565
- for (const [variantKey, variantConfig] of Object.entries(tenantOverrides.card.variants)) {
566
- if (!mcpTokens.card) continue;
567
- if (!mcpTokens.card.variants) mcpTokens.card.variants = {};
568
-
569
- mcpTokens.card.variants[variantKey] = {
570
- description: `${variantKey} variant (tenant override)`,
571
- tokens: transformConfigToTokens(variantConfig, "card"),
572
- };
573
- }
574
-
575
- return mcpTokens;
576
- }
577
-
578
- /**
579
- * Transform variant config to MCP token format
580
- */
581
- function transformConfigToTokens(config, componentType) {
582
- const tokens = {};
583
-
584
- for (const [key, value] of Object.entries(config)) {
585
- if (typeof value === "string") {
586
- tokens[key] = semanticToMCPPath(value);
587
- } else if (typeof value === "boolean") {
588
- // Skip boolean values for MCP tokens
589
- }
590
- }
591
-
592
- return tokens;
593
- }
594
-
595
- // ============================================
596
- // VALIDATION
597
- // ============================================
598
-
599
- function validateTokenReferences(components, tokenMap) {
600
- const errors = [];
601
- const warnings = [];
602
-
603
- // This is a simplified validation - in production you'd check each token
604
- // against the tokenMap to ensure it exists
605
-
606
- return { errors, warnings };
607
- }
608
-
609
- // ============================================
610
- // BREAKING CHANGE DETECTION
611
- // ============================================
612
-
613
- /**
614
- * Load the previous component tokens snapshot
615
- */
616
- function loadPreviousSnapshot() {
617
- try {
618
- // Ensure data directory exists
619
- const dataDir = path.dirname(SNAPSHOT_PATH);
620
- if (!fs.existsSync(dataDir)) {
621
- fs.mkdirSync(dataDir, { recursive: true });
622
- }
623
-
624
- if (!fs.existsSync(SNAPSHOT_PATH)) {
625
- return null;
626
- }
627
-
628
- return JSON.parse(fs.readFileSync(SNAPSHOT_PATH, "utf-8"));
629
- } catch (error) {
630
- console.warn(" Warning: Could not load previous snapshot:", error.message);
631
- return null;
632
- }
633
- }
634
-
635
- /**
636
- * Save current tokens as snapshot for future comparison
637
- */
638
- function saveSnapshot(tokens) {
639
- try {
640
- const dataDir = path.dirname(SNAPSHOT_PATH);
641
- if (!fs.existsSync(dataDir)) {
642
- fs.mkdirSync(dataDir, { recursive: true });
643
- }
644
-
645
- const snapshot = {
646
- version: "1.0.0",
647
- timestamp: new Date().toISOString(),
648
- tenantId: CONFIG.tenantId,
649
- components: tokens,
650
- };
651
-
652
- fs.writeFileSync(SNAPSHOT_PATH, JSON.stringify(snapshot, null, 2), "utf-8");
653
- } catch (error) {
654
- console.warn(" Warning: Could not save snapshot:", error.message);
655
- }
656
- }
657
-
658
- /**
659
- * Detect breaking changes between old and new tokens
660
- */
661
- function detectBreakingChanges(oldTokens, newTokens) {
662
- const changes = {
663
- removedComponents: [],
664
- removedVariants: [],
665
- removedSizes: [],
666
- renamedComponents: [],
667
- addedComponents: [],
668
- addedVariants: [],
669
- addedSizes: [],
670
- };
671
-
672
- if (!oldTokens) {
673
- // First run - no comparison possible
674
- return changes;
675
- }
676
-
677
- const oldComponentKeys = Object.keys(oldTokens);
678
- const newComponentKeys = Object.keys(newTokens);
679
-
680
- // Detect removed components
681
- for (const key of oldComponentKeys) {
682
- if (!newComponentKeys.includes(key)) {
683
- changes.removedComponents.push(key);
684
- }
685
- }
686
-
687
- // Detect added components
688
- for (const key of newComponentKeys) {
689
- if (!oldComponentKeys.includes(key)) {
690
- changes.addedComponents.push(key);
691
- }
692
- }
693
-
694
- // Detect removed/added variants and sizes
695
- for (const key of newComponentKeys) {
696
- if (!oldTokens[key]) continue;
697
-
698
- const oldComp = oldTokens[key];
699
- const newComp = newTokens[key];
700
-
701
- // Check variants
702
- if (oldComp.variants && newComp.variants) {
703
- const oldVariants = Object.keys(oldComp.variants);
704
- const newVariants = Object.keys(newComp.variants);
705
-
706
- for (const v of oldVariants) {
707
- if (!newVariants.includes(v)) {
708
- changes.removedVariants.push({ component: key, variant: v });
709
- }
710
- }
711
-
712
- for (const v of newVariants) {
713
- if (!oldVariants.includes(v)) {
714
- changes.addedVariants.push({ component: key, variant: v });
715
- }
716
- }
717
- }
718
-
719
- // Check sizes
720
- if (oldComp.sizes && newComp.sizes) {
721
- const oldSizes = Object.keys(oldComp.sizes);
722
- const newSizes = Object.keys(newComp.sizes);
723
-
724
- for (const s of oldSizes) {
725
- if (!newSizes.includes(s)) {
726
- changes.removedSizes.push({ component: key, size: s });
727
- }
728
- }
729
-
730
- for (const s of newSizes) {
731
- if (!oldSizes.includes(s)) {
732
- changes.addedSizes.push({ component: key, size: s });
733
- }
734
- }
735
- }
736
- }
737
-
738
- return changes;
739
- }
740
-
741
- /**
742
- * Report breaking changes with clear warnings
743
- */
744
- function reportBreakingChanges(changes) {
745
- const hasBreaking = changes.removedComponents.length > 0 ||
746
- changes.removedVariants.length > 0 ||
747
- changes.removedSizes.length > 0;
748
-
749
- const hasAdditions = changes.addedComponents.length > 0 ||
750
- changes.addedVariants.length > 0 ||
751
- changes.addedSizes.length > 0;
752
-
753
- if (!hasBreaking && !hasAdditions) {
754
- return;
755
- }
756
-
757
- console.log("");
758
-
759
- // Report breaking changes (removals)
760
- if (hasBreaking) {
761
- console.log("\x1b[33m ⚠ BREAKING CHANGES DETECTED:\x1b[0m");
762
-
763
- if (changes.removedComponents.length > 0) {
764
- console.log("\x1b[31m Removed components:\x1b[0m");
765
- for (const comp of changes.removedComponents) {
766
- console.log(` - ${comp}`);
767
- console.log(` \x1b[2mCode using <${comp.charAt(0).toUpperCase() + comp.slice(1)} /> will break\x1b[0m`);
768
- }
769
- }
770
-
771
- if (changes.removedVariants.length > 0) {
772
- console.log("\x1b[31m Removed variants:\x1b[0m");
773
- for (const { component, variant } of changes.removedVariants) {
774
- console.log(` - ${component}.${variant}`);
775
- console.log(` \x1b[2mCode using variant="${variant}" will break\x1b[0m`);
776
- }
777
- }
778
-
779
- if (changes.removedSizes.length > 0) {
780
- console.log("\x1b[31m Removed sizes:\x1b[0m");
781
- for (const { component, size } of changes.removedSizes) {
782
- console.log(` - ${component}.${size}`);
783
- console.log(` \x1b[2mCode using size="${size}" will break\x1b[0m`);
784
- }
785
- }
786
-
787
- console.log("");
788
- console.log("\x1b[33m Action required: Update consuming code or restore removed items.\x1b[0m");
789
- }
790
-
791
- // Report additions (non-breaking)
792
- if (hasAdditions) {
793
- console.log("\x1b[32m ✓ New additions:\x1b[0m");
794
-
795
- if (changes.addedComponents.length > 0) {
796
- console.log(` Components: ${changes.addedComponents.join(", ")}`);
797
- }
798
-
799
- if (changes.addedVariants.length > 0) {
800
- const grouped = {};
801
- for (const { component, variant } of changes.addedVariants) {
802
- if (!grouped[component]) grouped[component] = [];
803
- grouped[component].push(variant);
804
- }
805
- for (const [comp, variants] of Object.entries(grouped)) {
806
- console.log(` ${comp} variants: ${variants.join(", ")}`);
807
- }
808
- }
809
-
810
- if (changes.addedSizes.length > 0) {
811
- const grouped = {};
812
- for (const { component, size } of changes.addedSizes) {
813
- if (!grouped[component]) grouped[component] = [];
814
- grouped[component].push(size);
815
- }
816
- for (const [comp, sizes] of Object.entries(grouped)) {
817
- console.log(` ${comp} sizes: ${sizes.join(", ")}`);
818
- }
819
- }
820
- }
821
-
822
- console.log("");
823
-
824
- return hasBreaking;
825
- }
826
-
827
- // ============================================
828
- // OUTPUT GENERATION
829
- // ============================================
830
-
831
- function generateOutputFile(components, manifest, tenantId) {
832
- const timestamp = new Date().toISOString();
833
-
834
- return `/**
835
- * Component Token Mappings
836
- *
837
- * AUTO-GENERATED from apps/atomix-lab/src/config/component-defaults.ts
838
- * DO NOT EDIT MANUALLY - run \`npm run sync\` to regenerate.
839
- *
840
- * Maps each component to its design tokens.
841
- * This allows AI tools to understand which tokens apply to which components.
842
- *
843
- * Last synced: ${timestamp}
844
- * Tenant: ${tenantId}
845
- */
846
-
847
- export interface ComponentTokenConfig {
848
- description: string;
849
- variants?: Record<string, VariantTokens>;
850
- sizes?: Record<string, SizeTokens>;
851
- shared?: Record<string, string>;
852
- cssVariables?: string[];
853
- tailwindClasses?: string[];
854
- }
855
-
856
- export interface VariantTokens {
857
- description?: string;
858
- tokens: Record<string, string>;
859
- }
860
-
861
- export interface SizeTokens {
862
- description?: string;
863
- tokens: Record<string, string>;
864
- }
865
-
866
- // Multi-tenant ready token store
867
- export interface MCPTokenStore {
868
- version: string;
869
- tenantId: string;
870
- generatedAt: string;
871
- components: Record<string, ComponentTokenConfig>;
872
- }
873
-
874
- export const COMPONENT_TOKENS: Record<string, ComponentTokenConfig> = ${JSON.stringify(components, null, 2)};
875
-
876
- // Multi-tenant wrapper (backward compatible)
877
- export const TOKEN_STORE: MCPTokenStore = {
878
- version: "1.0.0",
879
- tenantId: "${tenantId}",
880
- generatedAt: "${timestamp}",
881
- components: COMPONENT_TOKENS,
882
- };
883
- `;
884
- }
885
-
886
- // ============================================
887
- // MAIN
888
- // ============================================
889
-
890
- async function main() {
891
- console.log("Syncing component tokens (dynamic parser)...");
892
- console.log(` Tenant: ${CONFIG.tenantId}`);
893
-
894
- // Future: If cloud API is configured, fetch from there
895
- if (CONFIG.cloudApiUrl) {
896
- console.log(` Cloud API: ${CONFIG.cloudApiUrl}`);
897
- // TODO: Implement cloud sync
898
- // const tenantData = await fetchFromCloud(CONFIG.tenantId);
899
- // syncFromCloudData(tenantData);
900
- // return;
901
- }
902
-
903
- // Check if source file exists
904
- if (!fs.existsSync(LAB_DEFAULTS_PATH)) {
905
- console.error(`Error: Could not find ${LAB_DEFAULTS_PATH}`);
906
- process.exit(1);
907
- }
908
-
909
- // Load previous snapshot for breaking change detection
910
- const previousSnapshot = loadPreviousSnapshot();
911
-
912
- // Load token map
913
- loadTokenMap();
914
-
915
- // Read and parse component defaults
916
- const content = fs.readFileSync(LAB_DEFAULTS_PATH, "utf-8");
917
-
918
- // Discover components dynamically
919
- const components = discoverComponents(content);
920
-
921
- // Parse manifest for metadata
922
- const manifest = parseManifest(content);
923
-
924
- // Generate MCP tokens from shared defaults
925
- let mcpTokens = generateMCPTokens(components, manifest, content);
926
-
927
- console.log(` Generated tokens for: ${Object.keys(mcpTokens).join(", ")}`);
928
-
929
- // Load and merge tenant overrides
930
- const tenantOverrides = loadTenantOverrides(CONFIG.tenantId);
931
- mcpTokens = mergeTenantOverrides(mcpTokens, tenantOverrides);
932
-
933
- // Detect breaking changes
934
- const oldTokens = previousSnapshot?.components || null;
935
- const changes = detectBreakingChanges(oldTokens, mcpTokens);
936
- const hasBreaking = reportBreakingChanges(changes);
937
-
938
- // Validate token references
939
- const { errors, warnings } = validateTokenReferences(mcpTokens, tokenMap);
940
-
941
- if (warnings.length > 0) {
942
- console.log("\n Warnings:");
943
- warnings.forEach(w => console.log(` - ${w}`));
944
- }
945
-
946
- if (errors.length > 0) {
947
- console.log("\n Errors:");
948
- errors.forEach(e => console.log(` - ${e}`));
949
- process.exit(1);
950
- }
951
-
952
- // Generate output file
953
- const output = generateOutputFile(mcpTokens, manifest, CONFIG.tenantId);
954
-
955
- // Write output
956
- fs.writeFileSync(MCP_TOKENS_PATH, output, "utf-8");
957
- console.log(` ✓ Written to ${MCP_TOKENS_PATH}`);
958
-
959
- // Save snapshot for future comparison
960
- saveSnapshot(mcpTokens);
961
-
962
- if (hasBreaking) {
963
- console.log("\n\x1b[33m Note: Breaking changes will affect existing code.\x1b[0m");
964
- console.log(" Run your test suite to identify affected components.\n");
965
- }
966
-
967
- console.log("\nTo apply changes, rebuild the MCP server:");
968
- console.log(" cd packages/atomix-mcp && npm run build");
969
- }
970
-
971
- main().catch((error) => {
972
- console.error("Error syncing component tokens:", error);
973
- process.exit(1);
974
- });