@gemini-designer/mcp-server 0.1.2 → 0.1.29

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 (129) hide show
  1. package/dist/components/catalog.d.ts.map +1 -1
  2. package/dist/components/catalog.js +10 -4
  3. package/dist/components/catalog.js.map +1 -1
  4. package/dist/config/index.d.ts.map +1 -1
  5. package/dist/config/index.js +11 -6
  6. package/dist/config/index.js.map +1 -1
  7. package/dist/context/builder.d.ts.map +1 -1
  8. package/dist/context/builder.js.map +1 -1
  9. package/dist/context/filter.d.ts.map +1 -1
  10. package/dist/context/filter.js +5 -1
  11. package/dist/context/filter.js.map +1 -1
  12. package/dist/context/grounding.d.ts.map +1 -1
  13. package/dist/context/grounding.js +7 -3
  14. package/dist/context/grounding.js.map +1 -1
  15. package/dist/context/guards.d.ts.map +1 -1
  16. package/dist/context/guards.js +53 -0
  17. package/dist/context/guards.js.map +1 -1
  18. package/dist/context/repo-hints.js.map +1 -1
  19. package/dist/context/styling-detector.d.ts +24 -0
  20. package/dist/context/styling-detector.d.ts.map +1 -0
  21. package/dist/context/styling-detector.js +337 -0
  22. package/dist/context/styling-detector.js.map +1 -0
  23. package/dist/design/principles.js.map +1 -1
  24. package/dist/generation/gemini-client.d.ts.map +1 -1
  25. package/dist/generation/gemini-client.js.map +1 -1
  26. package/dist/generation/litellm-client.d.ts.map +1 -1
  27. package/dist/generation/litellm-client.js +14 -7
  28. package/dist/generation/litellm-client.js.map +1 -1
  29. package/dist/generation/remote-client.d.ts +10 -5
  30. package/dist/generation/remote-client.d.ts.map +1 -1
  31. package/dist/generation/remote-client.js +13 -2
  32. package/dist/generation/remote-client.js.map +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/output/file-writer.d.ts.map +1 -1
  35. package/dist/output/file-writer.js +4 -4
  36. package/dist/output/file-writer.js.map +1 -1
  37. package/dist/output/formatter.d.ts.map +1 -1
  38. package/dist/output/formatter.js +5 -2
  39. package/dist/output/formatter.js.map +1 -1
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +2 -1
  42. package/dist/server.js.map +1 -1
  43. package/dist/stack/detect.d.ts.map +1 -1
  44. package/dist/stack/detect.js +42 -9
  45. package/dist/stack/detect.js.map +1 -1
  46. package/dist/tokens/sync.d.ts.map +1 -1
  47. package/dist/tokens/sync.js +22 -5
  48. package/dist/tokens/sync.js.map +1 -1
  49. package/dist/tools/analyze-screenshot-ui.d.ts.map +1 -1
  50. package/dist/tools/analyze-screenshot-ui.js +5 -5
  51. package/dist/tools/analyze-screenshot-ui.js.map +1 -1
  52. package/dist/tools/analyze-tokens.d.ts.map +1 -1
  53. package/dist/tools/analyze-tokens.js +3 -1
  54. package/dist/tools/analyze-tokens.js.map +1 -1
  55. package/dist/tools/catalog-components.d.ts.map +1 -1
  56. package/dist/tools/catalog-components.js +1 -4
  57. package/dist/tools/catalog-components.js.map +1 -1
  58. package/dist/tools/create-ui.d.ts +3 -0
  59. package/dist/tools/create-ui.d.ts.map +1 -1
  60. package/dist/tools/create-ui.js +203 -75
  61. package/dist/tools/create-ui.js.map +1 -1
  62. package/dist/tools/detect-ui-stack.js.map +1 -1
  63. package/dist/tools/generate-component-variants.d.ts.map +1 -1
  64. package/dist/tools/generate-component-variants.js +15 -4
  65. package/dist/tools/generate-component-variants.js.map +1 -1
  66. package/dist/tools/generate-vibes.d.ts.map +1 -1
  67. package/dist/tools/generate-vibes.js +7 -3
  68. package/dist/tools/generate-vibes.js.map +1 -1
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/modify-ui.d.ts.map +1 -1
  71. package/dist/tools/modify-ui.js +7 -2
  72. package/dist/tools/modify-ui.js.map +1 -1
  73. package/dist/tools/scaffold-project.d.ts.map +1 -1
  74. package/dist/tools/scaffold-project.js +3 -1
  75. package/dist/tools/scaffold-project.js.map +1 -1
  76. package/dist/tools/snippet-ui.d.ts +3 -1
  77. package/dist/tools/snippet-ui.d.ts.map +1 -1
  78. package/dist/tools/snippet-ui.js +219 -88
  79. package/dist/tools/snippet-ui.js.map +1 -1
  80. package/dist/tools/sync-design-tokens.d.ts.map +1 -1
  81. package/dist/tools/sync-design-tokens.js +26 -11
  82. package/dist/tools/sync-design-tokens.js.map +1 -1
  83. package/dist/utils/walk.d.ts.map +1 -1
  84. package/dist/utils/walk.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +5 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +55 -55
  90. package/src/__tests__/builder.test.ts +19 -19
  91. package/src/__tests__/config.test.ts +63 -31
  92. package/src/__tests__/filter.test.ts +98 -92
  93. package/src/__tests__/remote-client.test.ts +179 -0
  94. package/src/components/catalog.ts +170 -166
  95. package/src/config/index.ts +185 -177
  96. package/src/context/builder.ts +157 -157
  97. package/src/context/filter.ts +110 -104
  98. package/src/context/grounding.ts +143 -129
  99. package/src/context/guards.ts +97 -38
  100. package/src/context/repo-hints.ts +24 -24
  101. package/src/context/styling-detector.ts +460 -0
  102. package/src/design/principles.ts +14 -14
  103. package/src/generation/gemini-client.ts +53 -56
  104. package/src/generation/litellm-client.ts +102 -86
  105. package/src/generation/remote-client.ts +100 -77
  106. package/src/index.ts +16 -16
  107. package/src/output/file-writer.ts +123 -123
  108. package/src/output/formatter.ts +139 -132
  109. package/src/server.ts +12 -11
  110. package/src/stack/detect.ts +226 -175
  111. package/src/tokens/sync.ts +189 -155
  112. package/src/tools/analyze-screenshot-ui.ts +89 -88
  113. package/src/tools/analyze-tokens.ts +80 -78
  114. package/src/tools/catalog-components.ts +68 -68
  115. package/src/tools/create-ui.ts +295 -142
  116. package/src/tools/detect-ui-stack.ts +36 -36
  117. package/src/tools/generate-component-variants.ts +155 -135
  118. package/src/tools/generate-vibes.ts +121 -117
  119. package/src/tools/index.ts +14 -14
  120. package/src/tools/modify-ui.ts +170 -165
  121. package/src/tools/scaffold-project.ts +68 -66
  122. package/src/tools/snippet-ui.ts +323 -172
  123. package/src/tools/sync-design-tokens.ts +217 -195
  124. package/src/utils/walk.ts +47 -45
  125. package/src/version.ts +6 -0
  126. package/tsconfig.json +23 -33
  127. package/vitest.config.ts +10 -10
  128. package/.prettierrc +0 -9
  129. package/eslint.config.js +0 -37
@@ -17,41 +17,59 @@ import { assertReadablePath, assertWritablePath } from '../context/guards.js';
17
17
  import { sanitizeContent } from '../context/filter.js';
18
18
  import { writeFile } from '../output/file-writer.js';
19
19
  import {
20
- parseCssVars,
21
- tokensToCssVars,
22
- tokensToTailwindTheme,
23
- tokensToTokensStudio,
24
- tokensToStyleDictionary,
25
- type Token,
20
+ parseCssVars,
21
+ tokensToCssVars,
22
+ tokensToTailwindTheme,
23
+ tokensToTokensStudio,
24
+ tokensToStyleDictionary,
25
+ type TokenType,
26
+ type Token,
26
27
  } from '../tokens/sync.js';
27
28
  import { generateWithGemini } from '../generation/gemini-client.js';
28
29
 
30
+ const TOKEN_TYPES: ReadonlySet<TokenType> = new Set([
31
+ 'color',
32
+ 'dimension',
33
+ 'fontFamily',
34
+ 'fontWeight',
35
+ 'number',
36
+ 'string',
37
+ 'shadow',
38
+ 'duration',
39
+ 'easing',
40
+ ]);
41
+
42
+ function coerceTokenType(value: unknown): TokenType {
43
+ if (typeof value === 'string' && TOKEN_TYPES.has(value as TokenType)) return value as TokenType;
44
+ return 'string';
45
+ }
46
+
29
47
  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.'),
48
+ sourcePath: z.string().optional().describe('Path to a token source file (CSS vars or JSON).'),
49
+ sourceContent: z.string().optional().describe('Inline token content (if not using sourcePath).'),
50
+ sourceType: z
51
+ .enum(['css-vars', 'tokens-json', 'style-dictionary'])
52
+ .default('css-vars')
53
+ .describe('How to interpret the source.'),
54
+ targets: z
55
+ .array(z.enum(['css-vars', 'tailwind', 'tokens-studio', 'style-dictionary']))
56
+ .default(['css-vars', 'tailwind', 'tokens-studio'])
57
+ .describe('Target formats to generate.'),
58
+ aiNormalize: z
59
+ .boolean()
60
+ .default(false)
61
+ .describe('If true, use the model to improve token naming & grouping (uses tokens).'),
62
+ writeToFile: z.boolean().default(false).describe('If true, write generated outputs to files.'),
63
+ outputPaths: z
64
+ .object({
65
+ cssVars: z.string().optional(),
66
+ tailwind: z.string().optional(),
67
+ tokensStudio: z.string().optional(),
68
+ styleDictionary: z.string().optional(),
69
+ })
70
+ .optional()
71
+ .describe('Where to write outputs if writeToFile=true.'),
72
+ backup: z.boolean().default(true).describe('Create .bak backups for overwritten files.'),
55
73
  };
56
74
 
57
75
  const NORMALIZE_SYSTEM_PROMPT = `You are a design systems engineer. You will normalize and improve a design token set.
@@ -67,190 +85,194 @@ Output rules:
67
85
  `;
68
86
 
69
87
  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;
88
+ let out = response.trim();
89
+ const fenced = out.match(/```(?:json)?\n([\s\S]*?)```/);
90
+ if (fenced) out = fenced[1].trim();
91
+ return out;
74
92
  }
75
93
 
76
94
  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;
95
+ server.registerTool(
96
+ 'sync_design_tokens',
97
+ {
98
+ title: 'Sync Design Tokens',
99
+ description:
100
+ 'Convert tokens between CSS vars, Tailwind theme, Tokens Studio JSON, and Style Dictionary. Optionally writes outputs to files.',
101
+ inputSchema,
102
+ },
103
+ async (args) => {
104
+ const sourcePath = args.sourcePath as string | undefined;
105
+ const sourceContent = args.sourceContent as string | undefined;
106
+ const sourceType =
107
+ (args.sourceType as 'css-vars' | 'tokens-json' | 'style-dictionary') || 'css-vars';
108
+ const targets = (args.targets as Array<
109
+ 'css-vars' | 'tailwind' | 'tokens-studio' | 'style-dictionary'
110
+ >) || ['css-vars', 'tailwind', 'tokens-studio'];
111
+ const aiNormalize = args.aiNormalize === true;
112
+ const writeToFile = args.writeToFile === true;
113
+ const outputPaths = args.outputPaths as
114
+ | {
115
+ cssVars?: string;
116
+ tailwind?: string;
117
+ tokensStudio?: string;
118
+ styleDictionary?: string;
119
+ }
120
+ | undefined;
121
+ const backup = args.backup !== false;
105
122
 
106
- if (!sourcePath && !sourceContent) {
107
- return {
108
- content: [{ type: 'text' as const, text: 'Error: Provide either sourcePath or sourceContent.' }],
109
- isError: true,
110
- };
111
- }
123
+ if (!sourcePath && !sourceContent) {
124
+ return {
125
+ content: [
126
+ { type: 'text' as const, text: 'Error: Provide either sourcePath or sourceContent.' },
127
+ ],
128
+ isError: true,
129
+ };
130
+ }
112
131
 
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
- }
132
+ let raw: string;
133
+ let safeSourcePath: string | undefined;
134
+ try {
135
+ if (sourcePath) {
136
+ safeSourcePath = assertReadablePath(sourcePath, config);
137
+ raw = fs.readFileSync(safeSourcePath, 'utf-8');
138
+ } else {
139
+ raw = sourceContent as string;
140
+ }
141
+ } catch (error) {
142
+ const message = error instanceof Error ? error.message : 'Could not read source';
143
+ return {
144
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
145
+ isError: true,
146
+ };
147
+ }
129
148
 
130
- raw = sanitizeContent(raw);
149
+ raw = sanitizeContent(raw);
131
150
 
132
- let tokens: Token[] = [];
133
- const warnings: string[] = [];
151
+ let tokens: Token[] = [];
152
+ const warnings: string[] = [];
134
153
 
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[] = [];
154
+ try {
155
+ if (sourceType === 'css-vars') {
156
+ const parsed = parseCssVars(raw, safeSourcePath);
157
+ tokens = parsed.tokens;
158
+ warnings.push(...parsed.warnings);
159
+ } else {
160
+ // Very lightweight: accept either Tokens Studio or Style Dictionary style
161
+ const json = JSON.parse(raw);
162
+ const collected: Token[] = [];
144
163
 
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
- };
164
+ const walk = (obj: unknown, prefix: string[]) => {
165
+ if (!obj || typeof obj !== 'object') return;
161
166
 
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
- };
167
+ const rec = obj as Record<string, unknown>;
168
+ if (typeof rec.value === 'string' && typeof rec.type === 'string') {
169
+ collected.push({
170
+ name: [...prefix].join('.'),
171
+ value: rec.value,
172
+ type: coerceTokenType(rec.type),
173
+ description: typeof rec.description === 'string' ? rec.description : undefined,
174
+ source: { file: safeSourcePath ? safeSourcePath.split('/').pop() : undefined },
175
+ });
176
+ return;
177
+ }
178
+ for (const [k, v] of Object.entries(rec)) {
179
+ walk(v, [...prefix, k]);
172
180
  }
181
+ };
182
+
183
+ walk(json, []);
184
+ tokens = collected;
185
+ if (tokens.length === 0)
186
+ warnings.push('No tokens found in JSON (expected nested {value,type} nodes).');
187
+ }
188
+ } catch (error) {
189
+ const message = error instanceof Error ? error.message : 'Failed to parse tokens';
190
+ return {
191
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
192
+ isError: true,
193
+ };
194
+ }
173
195
 
174
- // Optional AI normalization
175
- if (aiNormalize && tokens.length > 0) {
176
- const userPrompt = `Normalize these tokens. Remember: never change values.
196
+ // Optional AI normalization
197
+ if (aiNormalize && tokens.length > 0) {
198
+ const userPrompt = `Normalize these tokens. Remember: never change values.
177
199
 
178
200
  TOKENS (JSON array):
179
201
  ${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
- }
202
+ try {
203
+ const resp = await generateWithGemini(config, NORMALIZE_SYSTEM_PROMPT, userPrompt, {
204
+ toolName: 'sync_design_tokens',
205
+ });
206
+ const cleaned = cleanJsonArray(resp);
207
+ const normalized = JSON.parse(cleaned);
208
+ if (Array.isArray(normalized)) {
209
+ tokens = normalized as Token[];
210
+ } else {
211
+ warnings.push('AI normalization returned non-array output; keeping original tokens.');
212
+ }
213
+ } catch (error) {
214
+ const message = error instanceof Error ? error.message : 'AI normalization failed';
215
+ warnings.push(`AI normalization failed: ${message}`);
216
+ }
217
+ }
196
218
 
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
- }
219
+ const outputs: Record<string, string> = {};
220
+ for (const t of targets) {
221
+ if (t === 'css-vars') outputs.cssVars = tokensToCssVars(tokens);
222
+ if (t === 'tailwind') outputs.tailwind = tokensToTailwindTheme(tokens);
223
+ if (t === 'tokens-studio') outputs.tokensStudio = tokensToTokensStudio(tokens);
224
+ if (t === 'style-dictionary') outputs.styleDictionary = tokensToStyleDictionary(tokens);
225
+ }
204
226
 
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;
227
+ // Optional writing
228
+ const written: Array<{ path: string; backupPath?: string }> = [];
229
+ if (writeToFile) {
230
+ const mapping = [
231
+ { outKey: 'cssVars', pathKey: 'cssVars' },
232
+ { outKey: 'tailwind', pathKey: 'tailwind' },
233
+ { outKey: 'tokensStudio', pathKey: 'tokensStudio' },
234
+ { outKey: 'styleDictionary', pathKey: 'styleDictionary' },
235
+ ] as const;
214
236
 
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
- }
237
+ for (const m of mapping) {
238
+ const content = outputs[m.outKey];
239
+ const outPath = outputPaths?.[m.pathKey];
240
+ if (!content || !outPath) continue;
241
+ try {
242
+ const safeOut = assertWritablePath(outPath, config);
243
+ const result = await writeFile(safeOut, content, { format: false, backup });
244
+ if (!result.success) {
245
+ return {
246
+ content: [
247
+ {
248
+ type: 'text' as const,
249
+ text: `Error: Failed writing ${outPath}: ${result.error || 'Unknown error'}`,
250
+ },
251
+ ],
252
+ isError: true,
253
+ };
242
254
  }
243
-
244
- const response = {
245
- tokens,
246
- outputs,
247
- warnings,
248
- written,
249
- };
250
-
255
+ written.push({ path: result.filePath, backupPath: result.backupPath || undefined });
256
+ } catch (error) {
257
+ const message = error instanceof Error ? error.message : 'Write failed';
251
258
  return {
252
- content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
259
+ content: [{ type: 'text' as const, text: `Error: ${message}` }],
260
+ isError: true,
253
261
  };
262
+ }
254
263
  }
255
- );
264
+ }
265
+
266
+ const response = {
267
+ tokens,
268
+ outputs,
269
+ warnings,
270
+ written,
271
+ };
272
+
273
+ return {
274
+ content: [{ type: 'text' as const, text: JSON.stringify(response, null, 2) }],
275
+ };
276
+ }
277
+ );
256
278
  }
package/src/utils/walk.ts CHANGED
@@ -10,66 +10,68 @@ import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
 
12
12
  export interface WalkOptions {
13
- includeExtensions?: string[];
14
- excludeDirNames?: string[];
15
- maxFiles?: number;
13
+ includeExtensions?: string[];
14
+ excludeDirNames?: string[];
15
+ maxFiles?: number;
16
16
  }
17
17
 
18
18
  const DEFAULT_EXCLUDE_DIRS = [
19
- 'node_modules',
20
- '.git',
21
- '.next',
22
- '.nuxt',
23
- 'dist',
24
- 'build',
25
- '.turbo',
26
- '.cache',
27
- 'coverage',
19
+ 'node_modules',
20
+ '.git',
21
+ '.next',
22
+ '.nuxt',
23
+ 'dist',
24
+ 'build',
25
+ '.turbo',
26
+ '.cache',
27
+ 'coverage',
28
28
  ];
29
29
 
30
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;
31
+ const includeExtensions = (options.includeExtensions || []).map((e) => e.toLowerCase());
32
+ const exclude = new Set(
33
+ (options.excludeDirNames || DEFAULT_EXCLUDE_DIRS).map((d) => d.toLowerCase())
34
+ );
35
+ const maxFiles = typeof options.maxFiles === 'number' ? options.maxFiles : 10_000;
34
36
 
35
- const results: string[] = [];
36
- const queue: string[] = [rootDir];
37
+ const results: string[] = [];
38
+ const queue: string[] = [rootDir];
37
39
 
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
- }
40
+ while (queue.length > 0) {
41
+ const current = queue.pop()!;
42
+ let entries: fs.Dirent[];
43
+ try {
44
+ entries = fs.readdirSync(current, { withFileTypes: true });
45
+ } catch {
46
+ continue;
47
+ }
46
48
 
47
- for (const ent of entries) {
48
- if (results.length >= maxFiles) return results;
49
- const abs = path.join(current, ent.name);
49
+ for (const ent of entries) {
50
+ if (results.length >= maxFiles) return results;
51
+ const abs = path.join(current, ent.name);
50
52
 
51
- if (ent.isDirectory()) {
52
- if (exclude.has(ent.name.toLowerCase())) continue;
53
- queue.push(abs);
54
- continue;
55
- }
53
+ if (ent.isDirectory()) {
54
+ if (exclude.has(ent.name.toLowerCase())) continue;
55
+ queue.push(abs);
56
+ continue;
57
+ }
56
58
 
57
- if (!ent.isFile()) continue;
58
- if (includeExtensions.length === 0) {
59
- results.push(abs);
60
- continue;
61
- }
59
+ if (!ent.isFile()) continue;
60
+ if (includeExtensions.length === 0) {
61
+ results.push(abs);
62
+ continue;
63
+ }
62
64
 
63
- const ext = path.extname(ent.name).toLowerCase();
64
- if (includeExtensions.includes(ext)) {
65
- results.push(abs);
66
- }
67
- }
65
+ const ext = path.extname(ent.name).toLowerCase();
66
+ if (includeExtensions.includes(ext)) {
67
+ results.push(abs);
68
+ }
68
69
  }
70
+ }
69
71
 
70
- return results;
72
+ return results;
71
73
  }
72
74
 
73
75
  export function toPosixPath(p: string): string {
74
- return p.replace(/\\/g, '/');
76
+ return p.replace(/\\/g, '/');
75
77
  }
package/src/version.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { createRequire } from 'node:module';
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const pkg = require('../package.json') as { version?: string };
5
+
6
+ export const VERSION = typeof pkg.version === 'string' ? pkg.version : '0.0.0';
package/tsconfig.json CHANGED
@@ -1,34 +1,24 @@
1
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
- }
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true,
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
24
+ }