@gemini-designer/mcp-server 0.1.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.
Files changed (153) hide show
  1. package/.prettierrc +9 -0
  2. package/dist/components/catalog.d.ts +24 -0
  3. package/dist/components/catalog.d.ts.map +1 -0
  4. package/dist/components/catalog.js +186 -0
  5. package/dist/components/catalog.js.map +1 -0
  6. package/dist/config/index.d.ts +60 -0
  7. package/dist/config/index.d.ts.map +1 -0
  8. package/dist/config/index.js +199 -0
  9. package/dist/config/index.js.map +1 -0
  10. package/dist/context/builder.d.ts +32 -0
  11. package/dist/context/builder.d.ts.map +1 -0
  12. package/dist/context/builder.js +194 -0
  13. package/dist/context/builder.js.map +1 -0
  14. package/dist/context/filter.d.ts +28 -0
  15. package/dist/context/filter.d.ts.map +1 -0
  16. package/dist/context/filter.js +136 -0
  17. package/dist/context/filter.js.map +1 -0
  18. package/dist/context/grounding.d.ts +27 -0
  19. package/dist/context/grounding.d.ts.map +1 -0
  20. package/dist/context/grounding.js +162 -0
  21. package/dist/context/grounding.js.map +1 -0
  22. package/dist/context/guards.d.ts +31 -0
  23. package/dist/context/guards.d.ts.map +1 -0
  24. package/dist/context/guards.js +76 -0
  25. package/dist/context/guards.js.map +1 -0
  26. package/dist/context/repo-hints.d.ts +12 -0
  27. package/dist/context/repo-hints.d.ts.map +1 -0
  28. package/dist/context/repo-hints.js +40 -0
  29. package/dist/context/repo-hints.js.map +1 -0
  30. package/dist/generation/gemini-client.d.ts +27 -0
  31. package/dist/generation/gemini-client.d.ts.map +1 -0
  32. package/dist/generation/gemini-client.js +64 -0
  33. package/dist/generation/gemini-client.js.map +1 -0
  34. package/dist/generation/litellm-client.d.ts +16 -0
  35. package/dist/generation/litellm-client.d.ts.map +1 -0
  36. package/dist/generation/litellm-client.js +98 -0
  37. package/dist/generation/litellm-client.js.map +1 -0
  38. package/dist/generation/remote-client.d.ts +20 -0
  39. package/dist/generation/remote-client.d.ts.map +1 -0
  40. package/dist/generation/remote-client.js +69 -0
  41. package/dist/generation/remote-client.js.map +1 -0
  42. package/dist/index.d.ts +9 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +30 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/output/file-writer.d.ts +39 -0
  47. package/dist/output/file-writer.d.ts.map +1 -0
  48. package/dist/output/file-writer.js +153 -0
  49. package/dist/output/file-writer.js.map +1 -0
  50. package/dist/output/formatter.d.ts +26 -0
  51. package/dist/output/formatter.d.ts.map +1 -0
  52. package/dist/output/formatter.js +156 -0
  53. package/dist/output/formatter.js.map +1 -0
  54. package/dist/server.d.ts +9 -0
  55. package/dist/server.d.ts.map +1 -0
  56. package/dist/server.js +22 -0
  57. package/dist/server.js.map +1 -0
  58. package/dist/stack/detect.d.ts +49 -0
  59. package/dist/stack/detect.d.ts.map +1 -0
  60. package/dist/stack/detect.js +157 -0
  61. package/dist/stack/detect.js.map +1 -0
  62. package/dist/tokens/sync.d.ts +32 -0
  63. package/dist/tokens/sync.d.ts.map +1 -0
  64. package/dist/tokens/sync.js +188 -0
  65. package/dist/tokens/sync.js.map +1 -0
  66. package/dist/tools/analyze-screenshot-ui.d.ts +18 -0
  67. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -0
  68. package/dist/tools/analyze-screenshot-ui.js +133 -0
  69. package/dist/tools/analyze-screenshot-ui.js.map +1 -0
  70. package/dist/tools/analyze-tokens.d.ts +10 -0
  71. package/dist/tools/analyze-tokens.d.ts.map +1 -0
  72. package/dist/tools/analyze-tokens.js +107 -0
  73. package/dist/tools/analyze-tokens.js.map +1 -0
  74. package/dist/tools/catalog-components.d.ts +14 -0
  75. package/dist/tools/catalog-components.d.ts.map +1 -0
  76. package/dist/tools/catalog-components.js +85 -0
  77. package/dist/tools/catalog-components.js.map +1 -0
  78. package/dist/tools/create-ui.d.ts +10 -0
  79. package/dist/tools/create-ui.d.ts.map +1 -0
  80. package/dist/tools/create-ui.js +167 -0
  81. package/dist/tools/create-ui.js.map +1 -0
  82. package/dist/tools/detect-ui-stack.d.ts +15 -0
  83. package/dist/tools/detect-ui-stack.d.ts.map +1 -0
  84. package/dist/tools/detect-ui-stack.js +52 -0
  85. package/dist/tools/detect-ui-stack.js.map +1 -0
  86. package/dist/tools/generate-component-variants.d.ts +15 -0
  87. package/dist/tools/generate-component-variants.d.ts.map +1 -0
  88. package/dist/tools/generate-component-variants.js +199 -0
  89. package/dist/tools/generate-component-variants.js.map +1 -0
  90. package/dist/tools/generate-vibes.d.ts +10 -0
  91. package/dist/tools/generate-vibes.d.ts.map +1 -0
  92. package/dist/tools/generate-vibes.js +145 -0
  93. package/dist/tools/generate-vibes.js.map +1 -0
  94. package/dist/tools/index.d.ts +12 -0
  95. package/dist/tools/index.d.ts.map +1 -0
  96. package/dist/tools/index.js +36 -0
  97. package/dist/tools/index.js.map +1 -0
  98. package/dist/tools/modify-ui.d.ts +11 -0
  99. package/dist/tools/modify-ui.d.ts.map +1 -0
  100. package/dist/tools/modify-ui.js +207 -0
  101. package/dist/tools/modify-ui.js.map +1 -0
  102. package/dist/tools/scaffold-project.d.ts +10 -0
  103. package/dist/tools/scaffold-project.d.ts.map +1 -0
  104. package/dist/tools/scaffold-project.js +122 -0
  105. package/dist/tools/scaffold-project.js.map +1 -0
  106. package/dist/tools/snippet-ui.d.ts +11 -0
  107. package/dist/tools/snippet-ui.d.ts.map +1 -0
  108. package/dist/tools/snippet-ui.js +194 -0
  109. package/dist/tools/snippet-ui.js.map +1 -0
  110. package/dist/tools/sync-design-tokens.d.ts +14 -0
  111. package/dist/tools/sync-design-tokens.d.ts.map +1 -0
  112. package/dist/tools/sync-design-tokens.js +233 -0
  113. package/dist/tools/sync-design-tokens.js.map +1 -0
  114. package/dist/utils/walk.d.ts +15 -0
  115. package/dist/utils/walk.d.ts.map +1 -0
  116. package/dist/utils/walk.js +63 -0
  117. package/dist/utils/walk.js.map +1 -0
  118. package/eslint.config.js +37 -0
  119. package/package.json +56 -0
  120. package/src/__tests__/builder.test.ts +31 -0
  121. package/src/__tests__/config.test.ts +52 -0
  122. package/src/__tests__/filter.test.ts +109 -0
  123. package/src/components/catalog.ts +214 -0
  124. package/src/config/index.ts +237 -0
  125. package/src/context/builder.ts +233 -0
  126. package/src/context/filter.ts +164 -0
  127. package/src/context/grounding.ts +191 -0
  128. package/src/context/guards.ts +94 -0
  129. package/src/context/repo-hints.ts +43 -0
  130. package/src/generation/gemini-client.ts +94 -0
  131. package/src/generation/litellm-client.ts +121 -0
  132. package/src/generation/remote-client.ts +103 -0
  133. package/src/index.ts +36 -0
  134. package/src/output/file-writer.ts +181 -0
  135. package/src/output/formatter.ts +186 -0
  136. package/src/server.ts +28 -0
  137. package/src/stack/detect.ts +204 -0
  138. package/src/tokens/sync.ts +212 -0
  139. package/src/tools/analyze-screenshot-ui.ts +150 -0
  140. package/src/tools/analyze-tokens.ts +123 -0
  141. package/src/tools/catalog-components.ts +99 -0
  142. package/src/tools/create-ui.ts +194 -0
  143. package/src/tools/detect-ui-stack.ts +64 -0
  144. package/src/tools/generate-component-variants.ts +218 -0
  145. package/src/tools/generate-vibes.ts +177 -0
  146. package/src/tools/index.ts +42 -0
  147. package/src/tools/modify-ui.ts +230 -0
  148. package/src/tools/scaffold-project.ts +138 -0
  149. package/src/tools/snippet-ui.ts +222 -0
  150. package/src/tools/sync-design-tokens.ts +256 -0
  151. package/src/utils/walk.ts +75 -0
  152. package/tsconfig.json +34 -0
  153. package/vitest.config.ts +15 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * snippet_ui Tool
3
+ *
4
+ * Generate standalone UI components (modals, tables, forms, etc.)
5
+ * for injection into an existing architecture.
6
+ * Can optionally write directly to a file if requested.
7
+ */
8
+
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { z } from 'zod';
11
+ import { Config } from '../config/index.js';
12
+ import { generateWithGemini } from '../generation/gemini-client.js';
13
+ import { buildContext } from '../context/builder.js';
14
+ import { buildRepoHints } from '../context/repo-hints.js';
15
+ import { assertWritablePath } from '../context/guards.js';
16
+ import { writeFile } from '../output/file-writer.js';
17
+
18
+ const inputSchema = {
19
+ type: z
20
+ .enum(['modal', 'table', 'chart', 'form', 'card', 'nav', 'hero', 'footer', 'sidebar', 'custom'])
21
+ .describe('Type of UI component to generate'),
22
+ description: z
23
+ .string()
24
+ .describe(
25
+ 'Detailed description of the component (e.g., "A data table with sorting, filtering, and pagination")'
26
+ ),
27
+ framework: z
28
+ .enum(['vanilla', 'react', 'vue', 'svelte', 'nextjs'])
29
+ .optional()
30
+ .describe('Target framework'),
31
+ context: z
32
+ .array(z.string())
33
+ .optional()
34
+ .describe('Paths to design tokens or existing components to match style'),
35
+ propsInterface: z
36
+ .string()
37
+ .optional()
38
+ .describe('TypeScript interface for component props (if applicable)'),
39
+ animated: z.boolean().default(false).describe('Include animations/transitions'),
40
+ targetPath: z.string().optional().describe('Where to write the output file (if writeToFile=true)'),
41
+ writeToFile: z.boolean().default(false).describe('If true, write the generated code to targetPath'),
42
+ };
43
+
44
+ function getSystemPrompt(type: string, framework: string): string {
45
+ return `You are a senior frontend engineer and UI designer.
46
+
47
+ Generate a standalone ${type} component in ${framework}.
48
+
49
+ Hard rules:
50
+ - Output ONLY the code. No explanations. No markdown. No code fences.
51
+ - Accessible by default (WCAG AA): semantic HTML, labels, aria-* where needed, keyboard support, focus-visible.
52
+ - Responsive by default (mobile-first).
53
+ - Do not add new dependencies unless explicitly requested.
54
+ - Avoid placeholder TODOs. Provide complete, production-ready implementation.
55
+
56
+ Type-specific requirements:
57
+ ${getComponentGuidelines(type)}`;
58
+ }
59
+
60
+ function getComponentGuidelines(type: string): string {
61
+ const guidelines: Record<string, string> = {
62
+ modal: [
63
+ '- Trap focus inside the modal',
64
+ '- Close on Escape',
65
+ '- Close on backdrop click',
66
+ '- Ensure aria-modal, role=dialog, accessible title/description wiring',
67
+ ].join('\n'),
68
+ table: [
69
+ '- Optional sortable columns (no external deps)',
70
+ '- Responsive: horizontal scroll or stacked layout on small screens',
71
+ '- Accessible headers and caption support',
72
+ ].join('\n'),
73
+ form: [
74
+ '- Accessible labels and error messages',
75
+ '- Validation states and disabled/loading submit state',
76
+ '- Keyboard submit behavior',
77
+ ].join('\n'),
78
+ card: ['- Flexible content slots', '- Hover/focus states', '- Optional media support'].join('\n'),
79
+ nav: [
80
+ '- Mobile hamburger pattern if needed',
81
+ '- Active state indication',
82
+ '- Keyboard navigation (tab, arrow keys if menus)',
83
+ ].join('\n'),
84
+ hero: ['- Responsive layout', '- CTA buttons', '- Ensure text contrast/legibility'].join('\n'),
85
+ footer: ['- Responsive multi-column layout', '- Social links', '- Legal/copyright'].join('\n'),
86
+ sidebar: ['- Collapsible (optional)', '- Active item highlighting', '- Nested items support'].join('\n'),
87
+ chart: [
88
+ '- Responsive sizing',
89
+ '- Provide an accessible table fallback for data',
90
+ '- Avoid heavy chart deps unless explicitly requested',
91
+ ].join('\n'),
92
+ custom: '- Follow best practices for the described component type',
93
+ };
94
+
95
+ return guidelines[type] || guidelines.custom;
96
+ }
97
+
98
+ /**
99
+ * Extract code from markdown code blocks.
100
+ * (We still instruct the model not to use fences; this is a safety net.)
101
+ */
102
+ function extractCode(response: string): string {
103
+ const codeBlockMatch = response.match(/```(?:\w+)?\n([\s\S]*?)```/);
104
+ if (codeBlockMatch) {
105
+ return codeBlockMatch[1].trim();
106
+ }
107
+ return response.trim();
108
+ }
109
+
110
+ export function registerSnippetUI(server: McpServer, config: Config): void {
111
+ server.registerTool(
112
+ 'snippet_ui',
113
+ {
114
+ title: 'Generate UI Snippet',
115
+ description:
116
+ 'Generate standalone UI components (modals, tables, forms, etc.) ready to inject. Set writeToFile=true to write directly.',
117
+ inputSchema,
118
+ },
119
+ async (args) => {
120
+ const type = args.type as string;
121
+ const description = args.description as string;
122
+ const framework =
123
+ (args.framework as 'vanilla' | 'react' | 'vue' | 'svelte' | 'nextjs') ||
124
+ config.defaultFramework;
125
+ const contextPaths = args.context as string[] | undefined;
126
+ const propsInterface = args.propsInterface as string | undefined;
127
+ const animated = args.animated === true;
128
+ const targetPath = args.targetPath as string | undefined;
129
+ const shouldWrite = args.writeToFile === true;
130
+
131
+ // Build context from existing files
132
+ let contextContent = '';
133
+ if (contextPaths && contextPaths.length > 0) {
134
+ contextContent = await buildContext(contextPaths, config);
135
+ }
136
+
137
+ const systemPrompt = getSystemPrompt(type, framework);
138
+
139
+ let userPrompt = `Create a ${type} component.
140
+
141
+ ${buildRepoHints(config)}
142
+
143
+ Description:
144
+ ${description}`;
145
+
146
+ if (propsInterface) {
147
+ userPrompt += `\n\nProps interface:\n${propsInterface}`;
148
+ }
149
+
150
+ if (animated) {
151
+ userPrompt += `\n\nInclude smooth, subtle animations/transitions (respect reduced motion).`;
152
+ }
153
+
154
+ if (contextContent) {
155
+ userPrompt += `\n\nMatch the style/tokens from these existing files:\n${contextContent}`;
156
+ }
157
+
158
+ try {
159
+ const response = await generateWithGemini(config, systemPrompt, userPrompt, { toolName: 'snippet_ui' });
160
+ const code = extractCode(response);
161
+
162
+ // Write to file if requested
163
+ if (shouldWrite) {
164
+ if (!targetPath) {
165
+ return {
166
+ content: [
167
+ {
168
+ type: 'text' as const,
169
+ text: `Error: writeToFile=true but no targetPath was provided.`,
170
+ },
171
+ ],
172
+ isError: true,
173
+ };
174
+ }
175
+
176
+ const safePath = assertWritablePath(targetPath, config);
177
+ const result = await writeFile(safePath, code, { format: true, backup: true });
178
+
179
+ if (!result.success) {
180
+ return {
181
+ content: [
182
+ {
183
+ type: 'text' as const,
184
+ text: `❌ Failed to write file: ${result.error || 'Unknown error'}`,
185
+ },
186
+ ],
187
+ isError: true,
188
+ };
189
+ }
190
+
191
+ const msg = [
192
+ `✅ Snippet written successfully.`,
193
+ `File: ${result.filePath}`,
194
+ result.backupPath ? `Backup: ${result.backupPath}` : undefined,
195
+ ]
196
+ .filter(Boolean)
197
+ .join('\n');
198
+
199
+ return {
200
+ content: [{ type: 'text' as const, text: msg }],
201
+ };
202
+ }
203
+
204
+ // Return code only (no wrappers) for best agent compatibility
205
+ return {
206
+ content: [{ type: 'text' as const, text: code }],
207
+ };
208
+ } catch (error) {
209
+ const message = error instanceof Error ? error.message : 'Unknown error';
210
+ return {
211
+ content: [
212
+ {
213
+ type: 'text' as const,
214
+ text: `Error generating snippet: ${message}`,
215
+ },
216
+ ],
217
+ isError: true,
218
+ };
219
+ }
220
+ }
221
+ );
222
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * sync_design_tokens Tool
3
+ *
4
+ * Converts design tokens between formats and optionally writes them to files.
5
+ *
6
+ * Primary use cases:
7
+ * - Take existing CSS vars and generate Tailwind theme extension
8
+ * - Export Tokens Studio JSON for Figma/Tokens Studio workflows
9
+ * - Normalize token files for team consistency
10
+ */
11
+
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import { z } from 'zod';
14
+ import * as fs from 'node:fs';
15
+ import { Config } from '../config/index.js';
16
+ import { assertReadablePath, assertWritablePath } from '../context/guards.js';
17
+ import { sanitizeContent } from '../context/filter.js';
18
+ import { writeFile } from '../output/file-writer.js';
19
+ import {
20
+ parseCssVars,
21
+ tokensToCssVars,
22
+ tokensToTailwindTheme,
23
+ tokensToTokensStudio,
24
+ tokensToStyleDictionary,
25
+ type Token,
26
+ } from '../tokens/sync.js';
27
+ import { generateWithGemini } from '../generation/gemini-client.js';
28
+
29
+ const inputSchema = {
30
+ sourcePath: z.string().optional().describe('Path to a token source file (CSS vars or JSON).'),
31
+ sourceContent: z.string().optional().describe('Inline token content (if not using sourcePath).'),
32
+ sourceType: z
33
+ .enum(['css-vars', 'tokens-json', 'style-dictionary'])
34
+ .default('css-vars')
35
+ .describe('How to interpret the source.'),
36
+ targets: z
37
+ .array(z.enum(['css-vars', 'tailwind', 'tokens-studio', 'style-dictionary']))
38
+ .default(['css-vars', 'tailwind', 'tokens-studio'])
39
+ .describe('Target formats to generate.'),
40
+ aiNormalize: z
41
+ .boolean()
42
+ .default(false)
43
+ .describe('If true, use the model to improve token naming & grouping (uses tokens).'),
44
+ writeToFile: z.boolean().default(false).describe('If true, write generated outputs to files.'),
45
+ outputPaths: z
46
+ .object({
47
+ cssVars: z.string().optional(),
48
+ tailwind: z.string().optional(),
49
+ tokensStudio: z.string().optional(),
50
+ styleDictionary: z.string().optional(),
51
+ })
52
+ .optional()
53
+ .describe('Where to write outputs if writeToFile=true.'),
54
+ backup: z.boolean().default(true).describe('Create .bak backups for overwritten files.'),
55
+ };
56
+
57
+ const NORMALIZE_SYSTEM_PROMPT = `You are a design systems engineer. You will normalize and improve a design token set.
58
+
59
+ Goal:
60
+ - Improve token naming and grouping using semantic names (primary, surface, text, border, danger, success)
61
+ - Preserve ALL token values exactly (do not change hex codes, sizes, etc.)
62
+ - Remove duplicates when values are identical AND names are redundant
63
+
64
+ Output rules:
65
+ - Output ONE valid JSON array of tokens only. No markdown. No prose.
66
+ - Each token: {"name": string, "value": string, "type": string, "description"?: string}
67
+ `;
68
+
69
+ function cleanJsonArray(response: string): string {
70
+ let out = response.trim();
71
+ const fenced = out.match(/```(?:json)?\n([\s\S]*?)```/);
72
+ if (fenced) out = fenced[1].trim();
73
+ return out;
74
+ }
75
+
76
+ export function registerSyncDesignTokens(server: McpServer, config: Config): void {
77
+ server.registerTool(
78
+ 'sync_design_tokens',
79
+ {
80
+ title: 'Sync Design Tokens',
81
+ description:
82
+ 'Convert tokens between CSS vars, Tailwind theme, Tokens Studio JSON, and Style Dictionary. Optionally writes outputs to files.',
83
+ inputSchema,
84
+ },
85
+ async (args) => {
86
+ const sourcePath = args.sourcePath as string | undefined;
87
+ const sourceContent = args.sourceContent as string | undefined;
88
+ const sourceType = (args.sourceType as 'css-vars' | 'tokens-json' | 'style-dictionary') || 'css-vars';
89
+ const targets = (args.targets as Array<'css-vars' | 'tailwind' | 'tokens-studio' | 'style-dictionary'>) || [
90
+ 'css-vars',
91
+ 'tailwind',
92
+ 'tokens-studio',
93
+ ];
94
+ const aiNormalize = args.aiNormalize === true;
95
+ const writeToFile = args.writeToFile === true;
96
+ const outputPaths = args.outputPaths as
97
+ | {
98
+ cssVars?: string;
99
+ tailwind?: string;
100
+ tokensStudio?: string;
101
+ styleDictionary?: string;
102
+ }
103
+ | undefined;
104
+ const backup = args.backup !== false;
105
+
106
+ if (!sourcePath && !sourceContent) {
107
+ return {
108
+ content: [{ type: 'text' as const, text: 'Error: Provide either sourcePath or sourceContent.' }],
109
+ isError: true,
110
+ };
111
+ }
112
+
113
+ let raw: string;
114
+ let safeSourcePath: string | undefined;
115
+ try {
116
+ if (sourcePath) {
117
+ safeSourcePath = assertReadablePath(sourcePath, config);
118
+ raw = fs.readFileSync(safeSourcePath, 'utf-8');
119
+ } else {
120
+ raw = sourceContent as string;
121
+ }
122
+ } catch (error) {
123
+ const message = error instanceof Error ? error.message : 'Could not read source';
124
+ return {
125
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
126
+ isError: true,
127
+ };
128
+ }
129
+
130
+ raw = sanitizeContent(raw);
131
+
132
+ let tokens: Token[] = [];
133
+ const warnings: string[] = [];
134
+
135
+ try {
136
+ if (sourceType === 'css-vars') {
137
+ const parsed = parseCssVars(raw, safeSourcePath);
138
+ tokens = parsed.tokens;
139
+ warnings.push(...parsed.warnings);
140
+ } else {
141
+ // Very lightweight: accept either Tokens Studio or Style Dictionary style
142
+ const json = JSON.parse(raw);
143
+ const collected: Token[] = [];
144
+
145
+ const walk = (obj: any, prefix: string[]) => {
146
+ if (!obj || typeof obj !== 'object') return;
147
+ if (typeof obj.value === 'string' && typeof obj.type === 'string') {
148
+ collected.push({
149
+ name: [...prefix].join('.'),
150
+ value: obj.value,
151
+ type: obj.type,
152
+ description: typeof obj.description === 'string' ? obj.description : undefined,
153
+ source: { file: safeSourcePath ? safeSourcePath.split('/').pop() : undefined },
154
+ });
155
+ return;
156
+ }
157
+ for (const [k, v] of Object.entries(obj)) {
158
+ walk(v, [...prefix, k]);
159
+ }
160
+ };
161
+
162
+ walk(json, []);
163
+ tokens = collected;
164
+ if (tokens.length === 0) warnings.push('No tokens found in JSON (expected nested {value,type} nodes).');
165
+ }
166
+ } catch (error) {
167
+ const message = error instanceof Error ? error.message : 'Failed to parse tokens';
168
+ return {
169
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
170
+ isError: true,
171
+ };
172
+ }
173
+
174
+ // Optional AI normalization
175
+ if (aiNormalize && tokens.length > 0) {
176
+ const userPrompt = `Normalize these tokens. Remember: never change values.
177
+
178
+ TOKENS (JSON array):
179
+ ${JSON.stringify(tokens, null, 2)}`;
180
+ try {
181
+ const resp = await generateWithGemini(config, NORMALIZE_SYSTEM_PROMPT, userPrompt, {
182
+ toolName: 'sync_design_tokens',
183
+ });
184
+ const cleaned = cleanJsonArray(resp);
185
+ const normalized = JSON.parse(cleaned);
186
+ if (Array.isArray(normalized)) {
187
+ tokens = normalized as Token[];
188
+ } else {
189
+ warnings.push('AI normalization returned non-array output; keeping original tokens.');
190
+ }
191
+ } catch (error) {
192
+ const message = error instanceof Error ? error.message : 'AI normalization failed';
193
+ warnings.push(`AI normalization failed: ${message}`);
194
+ }
195
+ }
196
+
197
+ const outputs: Record<string, string> = {};
198
+ for (const t of targets) {
199
+ if (t === 'css-vars') outputs.cssVars = tokensToCssVars(tokens);
200
+ if (t === 'tailwind') outputs.tailwind = tokensToTailwindTheme(tokens);
201
+ if (t === 'tokens-studio') outputs.tokensStudio = tokensToTokensStudio(tokens);
202
+ if (t === 'style-dictionary') outputs.styleDictionary = tokensToStyleDictionary(tokens);
203
+ }
204
+
205
+ // Optional writing
206
+ const written: Array<{ path: string; backupPath?: string }> = [];
207
+ if (writeToFile) {
208
+ const mapping = [
209
+ { outKey: 'cssVars', pathKey: 'cssVars' },
210
+ { outKey: 'tailwind', pathKey: 'tailwind' },
211
+ { outKey: 'tokensStudio', pathKey: 'tokensStudio' },
212
+ { outKey: 'styleDictionary', pathKey: 'styleDictionary' },
213
+ ] as const;
214
+
215
+ for (const m of mapping) {
216
+ const content = outputs[m.outKey];
217
+ const outPath = outputPaths?.[m.pathKey];
218
+ if (!content || !outPath) continue;
219
+ try {
220
+ const safeOut = assertWritablePath(outPath, config);
221
+ const result = await writeFile(safeOut, content, { format: false, backup });
222
+ if (!result.success) {
223
+ return {
224
+ content: [
225
+ {
226
+ type: 'text' as const,
227
+ text: `Error: Failed writing ${outPath}: ${result.error || 'Unknown error'}`,
228
+ },
229
+ ],
230
+ isError: true,
231
+ };
232
+ }
233
+ written.push({ path: result.filePath, backupPath: result.backupPath || undefined });
234
+ } catch (error) {
235
+ const message = error instanceof Error ? error.message : 'Write failed';
236
+ return {
237
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
238
+ isError: true,
239
+ };
240
+ }
241
+ }
242
+ }
243
+
244
+ const response = {
245
+ tokens,
246
+ outputs,
247
+ warnings,
248
+ written,
249
+ };
250
+
251
+ return {
252
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
253
+ };
254
+ }
255
+ );
256
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * walk.ts
3
+ *
4
+ * Lightweight recursive file walker with sane defaults.
5
+ * - No extra deps (no glob)
6
+ * - Skips common large / sensitive directories
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+
12
+ export interface WalkOptions {
13
+ includeExtensions?: string[];
14
+ excludeDirNames?: string[];
15
+ maxFiles?: number;
16
+ }
17
+
18
+ const DEFAULT_EXCLUDE_DIRS = [
19
+ 'node_modules',
20
+ '.git',
21
+ '.next',
22
+ '.nuxt',
23
+ 'dist',
24
+ 'build',
25
+ '.turbo',
26
+ '.cache',
27
+ 'coverage',
28
+ ];
29
+
30
+ export function walkFiles(rootDir: string, options: WalkOptions = {}): string[] {
31
+ const includeExtensions = (options.includeExtensions || []).map((e) => e.toLowerCase());
32
+ const exclude = new Set((options.excludeDirNames || DEFAULT_EXCLUDE_DIRS).map((d) => d.toLowerCase()));
33
+ const maxFiles = typeof options.maxFiles === 'number' ? options.maxFiles : 10_000;
34
+
35
+ const results: string[] = [];
36
+ const queue: string[] = [rootDir];
37
+
38
+ while (queue.length > 0) {
39
+ const current = queue.pop()!;
40
+ let entries: fs.Dirent[];
41
+ try {
42
+ entries = fs.readdirSync(current, { withFileTypes: true });
43
+ } catch {
44
+ continue;
45
+ }
46
+
47
+ for (const ent of entries) {
48
+ if (results.length >= maxFiles) return results;
49
+ const abs = path.join(current, ent.name);
50
+
51
+ if (ent.isDirectory()) {
52
+ if (exclude.has(ent.name.toLowerCase())) continue;
53
+ queue.push(abs);
54
+ continue;
55
+ }
56
+
57
+ if (!ent.isFile()) continue;
58
+ if (includeExtensions.length === 0) {
59
+ results.push(abs);
60
+ continue;
61
+ }
62
+
63
+ const ext = path.extname(ent.name).toLowerCase();
64
+ if (includeExtensions.includes(ext)) {
65
+ results.push(abs);
66
+ }
67
+ }
68
+ }
69
+
70
+ return results;
71
+ }
72
+
73
+ export function toPosixPath(p: string): string {
74
+ return p.replace(/\\/g, '/');
75
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2022"
8
+ ],
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "sourceMap": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": [
22
+ "src/*"
23
+ ]
24
+ }
25
+ },
26
+ "include": [
27
+ "src/**/*"
28
+ ],
29
+ "exclude": [
30
+ "node_modules",
31
+ "dist",
32
+ "**/*.test.ts"
33
+ ]
34
+ }
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ include: ['src/**/*.ts'],
12
+ exclude: ['src/**/*.test.ts', 'src/index.ts'],
13
+ },
14
+ },
15
+ });