sonance-brand-mcp 1.3.15 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/api/sonance-analyze/route.ts +1 -1
- package/dist/assets/api/sonance-save-logo/route.ts +2 -2
- package/dist/assets/brand-system.ts +4 -1
- package/dist/assets/components/image.tsx +3 -1
- package/dist/assets/components/select.tsx +3 -0
- package/dist/assets/dev-tools/SonanceDevTools.tsx +1837 -3579
- package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +230 -0
- package/dist/assets/dev-tools/components/ChatInterface.tsx +455 -0
- package/dist/assets/dev-tools/components/DiffPreview.tsx +190 -0
- package/dist/assets/dev-tools/components/InspectorOverlay.tsx +353 -0
- package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +199 -0
- package/dist/assets/dev-tools/components/VisionModeBorder.tsx +116 -0
- package/dist/assets/dev-tools/components/common.tsx +94 -0
- package/dist/assets/dev-tools/constants.ts +616 -0
- package/dist/assets/dev-tools/index.ts +29 -8
- package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +329 -0
- package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +623 -0
- package/dist/assets/dev-tools/panels/LogoToolsPanel.tsx +621 -0
- package/dist/assets/dev-tools/panels/LogosPanel.tsx +16 -0
- package/dist/assets/dev-tools/panels/TextPanel.tsx +332 -0
- package/dist/assets/dev-tools/types.ts +295 -0
- package/dist/assets/dev-tools/utils.ts +360 -0
- package/dist/index.js +268 -0
- package/package.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
ComponentStyle,
|
|
4
|
+
ConfigSection
|
|
5
|
+
} from "./types";
|
|
6
|
+
import {
|
|
7
|
+
SONANCE_PREVIEW_STYLE_ID,
|
|
8
|
+
TAILWIND_TO_CSS,
|
|
9
|
+
TARGETABLE_CATEGORIES,
|
|
10
|
+
COMPONENT_CONFIG_MAP
|
|
11
|
+
} from "./constants";
|
|
12
|
+
import { componentSnippets } from "../../lib/brand-system";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate CSS from component overrides
|
|
16
|
+
* Uses attribute selectors to target specific components
|
|
17
|
+
* Text color cascades to ALL descendants to override nested styles
|
|
18
|
+
*/
|
|
19
|
+
export function generateComponentCSS(overrides: Record<string, ComponentStyle>): string {
|
|
20
|
+
const rules: string[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [key, styles] of Object.entries(overrides)) {
|
|
23
|
+
const containerDeclarations: string[] = [];
|
|
24
|
+
const textDeclarations: string[] = [];
|
|
25
|
+
|
|
26
|
+
// Parse key to see if it's a variant override
|
|
27
|
+
// format: "componentName" or "componentName:variantId"
|
|
28
|
+
const [componentName, variantId] = key.split(':');
|
|
29
|
+
|
|
30
|
+
// Construct selector
|
|
31
|
+
// If variantId exists, target specific variant. Otherwise target all components of this type.
|
|
32
|
+
const selector = variantId
|
|
33
|
+
? `[data-sonance-name="${componentName}"][data-sonance-variant="${variantId}"]`
|
|
34
|
+
: `[data-sonance-name="${componentName}"]`;
|
|
35
|
+
|
|
36
|
+
// Container-level styles
|
|
37
|
+
if (styles.backgroundColor) {
|
|
38
|
+
containerDeclarations.push(`background-color: ${styles.backgroundColor} !important`);
|
|
39
|
+
}
|
|
40
|
+
if (styles.borderRadius) {
|
|
41
|
+
containerDeclarations.push(`border-radius: ${styles.borderRadius} !important`);
|
|
42
|
+
}
|
|
43
|
+
if (styles.borderColor) {
|
|
44
|
+
containerDeclarations.push(`border-color: ${styles.borderColor} !important`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Text styles - need to cascade to ALL children
|
|
48
|
+
if (styles.textColor) {
|
|
49
|
+
textDeclarations.push(`color: ${styles.textColor} !important`);
|
|
50
|
+
}
|
|
51
|
+
if (styles.fontWeight) {
|
|
52
|
+
textDeclarations.push(`font-weight: ${styles.fontWeight} !important`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Container styles on the element itself
|
|
56
|
+
if (containerDeclarations.length > 0) {
|
|
57
|
+
rules.push(`${selector} { ${containerDeclarations.join("; ")}; }`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Text styles on the element AND all descendants (to override child text styles)
|
|
61
|
+
if (textDeclarations.length > 0) {
|
|
62
|
+
// Apply to container itself
|
|
63
|
+
rules.push(`${selector} { ${textDeclarations.join("; ")}; }`);
|
|
64
|
+
// Apply to ALL descendants (p, span, h1-h6, a, etc.)
|
|
65
|
+
rules.push(`${selector} * { ${textDeclarations.join("; ")}; }`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Also handle generic element names (e.g., "button" without specific type)
|
|
69
|
+
if (componentName.includes("-")) {
|
|
70
|
+
const genericName = componentName.split("-")[0];
|
|
71
|
+
const genericSelector = `[data-sonance-name="${genericName}"]`;
|
|
72
|
+
|
|
73
|
+
if (containerDeclarations.length > 0) {
|
|
74
|
+
rules.push(`${genericSelector} { ${containerDeclarations.join("; ")}; }`);
|
|
75
|
+
}
|
|
76
|
+
if (textDeclarations.length > 0) {
|
|
77
|
+
rules.push(`${genericSelector} { ${textDeclarations.join("; ")}; }`);
|
|
78
|
+
rules.push(`${genericSelector} * { ${textDeclarations.join("; ")}; }`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return rules.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Inject or update preview styles in the document head
|
|
88
|
+
*/
|
|
89
|
+
export function injectPreviewStyles(css: string): void {
|
|
90
|
+
if (typeof document === "undefined") return;
|
|
91
|
+
|
|
92
|
+
let styleEl = document.getElementById(SONANCE_PREVIEW_STYLE_ID) as HTMLStyleElement | null;
|
|
93
|
+
|
|
94
|
+
if (!styleEl) {
|
|
95
|
+
styleEl = document.createElement("style");
|
|
96
|
+
styleEl.id = SONANCE_PREVIEW_STYLE_ID;
|
|
97
|
+
document.head.appendChild(styleEl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
styleEl.textContent = css;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Remove preview styles from the document
|
|
105
|
+
*/
|
|
106
|
+
export function removePreviewStyles(): void {
|
|
107
|
+
if (typeof document === "undefined") return;
|
|
108
|
+
|
|
109
|
+
const styleEl = document.getElementById(SONANCE_PREVIEW_STYLE_ID);
|
|
110
|
+
if (styleEl) {
|
|
111
|
+
styleEl.remove();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Convert a single Tailwind class to CSS
|
|
117
|
+
*/
|
|
118
|
+
export function tailwindClassToCSS(className: string): string {
|
|
119
|
+
// Handle hover/focus variants
|
|
120
|
+
const isHover = className.startsWith("hover:");
|
|
121
|
+
const isFocus = className.startsWith("focus:");
|
|
122
|
+
const baseClass = className.replace(/^(hover:|focus:)/, "");
|
|
123
|
+
|
|
124
|
+
let css = "";
|
|
125
|
+
|
|
126
|
+
for (const [prefix, converter] of Object.entries(TAILWIND_TO_CSS)) {
|
|
127
|
+
if (baseClass.startsWith(prefix)) {
|
|
128
|
+
const value = baseClass.slice(prefix.length);
|
|
129
|
+
css = converter(value);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
// Handle exact matches (like "shadow" without suffix)
|
|
133
|
+
if (baseClass === prefix.replace(/-$/, "")) {
|
|
134
|
+
css = converter("");
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return css;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract new Tailwind classes from modified code and convert to CSS
|
|
144
|
+
* Compares original and modified className strings
|
|
145
|
+
*/
|
|
146
|
+
export function extractPreviewCSSFromCode(
|
|
147
|
+
originalCode: string,
|
|
148
|
+
modifiedCode: string,
|
|
149
|
+
componentType: string,
|
|
150
|
+
variantId?: string | null
|
|
151
|
+
): string {
|
|
152
|
+
// Extract all className strings from both versions
|
|
153
|
+
const classNameRegex = /className\s*=\s*["'`]([^"'`]+)["'`]/g;
|
|
154
|
+
const cnRegex = /cn\s*\(\s*["'`]([^"'`]+)["'`]/g;
|
|
155
|
+
|
|
156
|
+
const extractClasses = (code: string): Set<string> => {
|
|
157
|
+
const classes = new Set<string>();
|
|
158
|
+
let match;
|
|
159
|
+
|
|
160
|
+
// Match className="..."
|
|
161
|
+
while ((match = classNameRegex.exec(code)) !== null) {
|
|
162
|
+
match[1].split(/\s+/).forEach(c => classes.add(c));
|
|
163
|
+
}
|
|
164
|
+
classNameRegex.lastIndex = 0;
|
|
165
|
+
|
|
166
|
+
// Match cn("...")
|
|
167
|
+
while ((match = cnRegex.exec(code)) !== null) {
|
|
168
|
+
match[1].split(/\s+/).forEach(c => classes.add(c));
|
|
169
|
+
}
|
|
170
|
+
cnRegex.lastIndex = 0;
|
|
171
|
+
|
|
172
|
+
return classes;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const originalClasses = extractClasses(originalCode);
|
|
176
|
+
const modifiedClasses = extractClasses(modifiedCode);
|
|
177
|
+
|
|
178
|
+
// Extract variant-specific conditional classes from modified code
|
|
179
|
+
// Matches patterns like: variant === "variant-7cea9f37" && "bg-white border-gray-300..."
|
|
180
|
+
// Also catches the short variantId (without "variant-" prefix)
|
|
181
|
+
let variantSpecificClasses: string[] = [];
|
|
182
|
+
if (variantId) {
|
|
183
|
+
// Try to match with full variant ID or just the hash part
|
|
184
|
+
// Note: We avoid backticks in patterns due to template literal parsing issues
|
|
185
|
+
const patterns = [
|
|
186
|
+
// Pattern: variant === "variant-7cea9f37" && "classes..."
|
|
187
|
+
new RegExp('variant\\s*===\\s*["\']variant-' + variantId + '["\']\\s*&&\\s*["\']([^"\']+)["\']', "g"),
|
|
188
|
+
// Pattern: variant === "7cea9f37" && "classes..."
|
|
189
|
+
new RegExp('variant\\s*===\\s*["\']' + variantId + '["\']\\s*&&\\s*["\']([^"\']+)["\']', "g"),
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const regex of patterns) {
|
|
193
|
+
let match;
|
|
194
|
+
while ((match = regex.exec(modifiedCode)) !== null) {
|
|
195
|
+
const classes = match[1].split(/\s+/).filter(c => c.length > 0);
|
|
196
|
+
variantSpecificClasses = [...variantSpecificClasses, ...classes];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Also try to find classes added in conditional blocks referencing our variant
|
|
201
|
+
// This catches cases where the exact pattern varies
|
|
202
|
+
const loosePattern = new RegExp('["\']variant-' + variantId + '["\'][^}]*["\']([^"\']{10,})["\']', "g");
|
|
203
|
+
let looseMatch;
|
|
204
|
+
while ((looseMatch = loosePattern.exec(modifiedCode)) !== null) {
|
|
205
|
+
// Extract potential Tailwind classes (words with hyphens, common patterns)
|
|
206
|
+
const potentialClasses = looseMatch[1].split(/\s+/).filter(c =>
|
|
207
|
+
c.length > 0 &&
|
|
208
|
+
(c.includes("-") || /^(bg|text|border|rounded|p|m|flex|grid|w|h|font|shadow|opacity|transition|hover|focus)/.test(c))
|
|
209
|
+
);
|
|
210
|
+
variantSpecificClasses = [...variantSpecificClasses, ...potentialClasses];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Deduplicate
|
|
214
|
+
variantSpecificClasses = [...new Set(variantSpecificClasses)];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Find new classes (in modified but not in original)
|
|
218
|
+
const newClasses = [...modifiedClasses].filter(c => !originalClasses.has(c));
|
|
219
|
+
|
|
220
|
+
if (newClasses.length === 0 && variantSpecificClasses.length === 0) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Convert new classes to CSS
|
|
225
|
+
const normalRules: string[] = [];
|
|
226
|
+
const hoverRules: string[] = [];
|
|
227
|
+
const variantRules: string[] = [];
|
|
228
|
+
|
|
229
|
+
// Process standard new classes
|
|
230
|
+
for (const className of newClasses) {
|
|
231
|
+
const css = tailwindClassToCSS(className);
|
|
232
|
+
if (css) {
|
|
233
|
+
if (className.startsWith("hover:")) {
|
|
234
|
+
hoverRules.push(css);
|
|
235
|
+
} else {
|
|
236
|
+
normalRules.push(css);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Process variant-specific classes (force apply to variant)
|
|
242
|
+
for (const className of variantSpecificClasses) {
|
|
243
|
+
const css = tailwindClassToCSS(className);
|
|
244
|
+
if (css) {
|
|
245
|
+
// For variant-specific, we treat everything as important override
|
|
246
|
+
variantRules.push(css);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const selector = `[data-sonance-name="${componentType}"]`;
|
|
251
|
+
const rules: string[] = [];
|
|
252
|
+
|
|
253
|
+
if (normalRules.length > 0) {
|
|
254
|
+
rules.push(`${selector} { ${normalRules.join("; ")} !important; }`);
|
|
255
|
+
}
|
|
256
|
+
if (hoverRules.length > 0) {
|
|
257
|
+
rules.push(`${selector}:hover { ${hoverRules.join("; ")} !important; }`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Force-apply variant-specific styles to the currently selected variant
|
|
261
|
+
if (variantId && variantRules.length > 0) {
|
|
262
|
+
const variantSelector = `[data-sonance-variant="${variantId}"]`;
|
|
263
|
+
// Apply to the element itself
|
|
264
|
+
rules.push(`${variantSelector} { ${variantRules.join(" !important; ")} !important; }`);
|
|
265
|
+
// Also try to apply text colors to children if they are set
|
|
266
|
+
const textRules = variantRules.filter(r => r.startsWith("color:"));
|
|
267
|
+
if (textRules.length > 0) {
|
|
268
|
+
rules.push(`${variantSelector} * { ${textRules.join(" !important; ")} !important; }`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Fallback: if we have a variantId but no variant-specific classes were detected,
|
|
273
|
+
// AND we have new normal classes, apply those to the variant selector as well
|
|
274
|
+
// This handles cases where the AI simply added classes without using the variant conditional pattern
|
|
275
|
+
if (variantId && variantRules.length === 0 && normalRules.length > 0) {
|
|
276
|
+
const variantSelector = `[data-sonance-variant="${variantId}"]`;
|
|
277
|
+
rules.push(`${variantSelector} { ${normalRules.join(" !important; ")} !important; }`);
|
|
278
|
+
// Apply text colors to children
|
|
279
|
+
const textRules = normalRules.filter(r => r.startsWith("color:"));
|
|
280
|
+
if (textRules.length > 0) {
|
|
281
|
+
rules.push(`${variantSelector} * { ${textRules.join(" !important; ")} !important; }`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Detect Tailwind data-attribute variants in multiple formats:
|
|
286
|
+
// Format 1: data-[variant='xxx']:rounded-lg
|
|
287
|
+
// Format 2: [&[data-variant='xxx']]:rounded-lg (Tailwind parent selector syntax)
|
|
288
|
+
const patterns = [
|
|
289
|
+
// Format 1: data-[variant='xxx']:class
|
|
290
|
+
/data-\[variant=['"]([^'"]+)['"]\]:([a-zA-Z0-9-]+)/g,
|
|
291
|
+
// Format 2: [&[data-variant='xxx']]:class
|
|
292
|
+
/\[&\[data-variant=['"]([^'"]+)['"]\]\]:([a-zA-Z0-9-]+)/g,
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
for (const pattern of patterns) {
|
|
296
|
+
let dataMatch;
|
|
297
|
+
while ((dataMatch = pattern.exec(modifiedCode)) !== null) {
|
|
298
|
+
const variantValue = dataMatch[1];
|
|
299
|
+
const tailwindClass = dataMatch[2];
|
|
300
|
+
const css = tailwindClassToCSS(tailwindClass);
|
|
301
|
+
if (css) {
|
|
302
|
+
// Generate CSS selectors that target BOTH:
|
|
303
|
+
// 1. data-variant (what the AI adds)
|
|
304
|
+
// 2. data-sonance-variant (what's already on the page from DevTools tagging)
|
|
305
|
+
// This ensures preview works before saving
|
|
306
|
+
rules.push(`[data-variant="${variantValue}"] { ${css} !important; }`);
|
|
307
|
+
rules.push(`[data-sonance-variant="${variantValue}"] { ${css} !important; }`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Also check if the variantId was passed - apply new classes to it directly
|
|
313
|
+
// This is a fallback when the AI doesn't use the data-variant pattern
|
|
314
|
+
if (variantId && rules.length === 0) {
|
|
315
|
+
// Try to extract any new Tailwind classes and apply to the variant
|
|
316
|
+
const allNewClasses = [...newClasses, ...variantSpecificClasses];
|
|
317
|
+
const variantCSS: string[] = [];
|
|
318
|
+
for (const className of allNewClasses) {
|
|
319
|
+
const css = tailwindClassToCSS(className);
|
|
320
|
+
if (css) {
|
|
321
|
+
variantCSS.push(css);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (variantCSS.length > 0) {
|
|
325
|
+
rules.push(`[data-sonance-variant="${variantId}"] { ${variantCSS.join(" !important; ")} !important; }`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return rules.join("\n");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Helper to check if a category should show scope options
|
|
333
|
+
export function shouldShowScopeOptions(category: string): boolean {
|
|
334
|
+
if (category === "all") return true; // Show for "all" selection
|
|
335
|
+
const lowerCategory = category.toLowerCase();
|
|
336
|
+
return TARGETABLE_CATEGORIES.includes(lowerCategory);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get the visible config sections for a given component type
|
|
341
|
+
* First checks for specific component ID, then falls back to category
|
|
342
|
+
*/
|
|
343
|
+
export function getVisibleSections(componentType: string): ConfigSection[] {
|
|
344
|
+
// Check for exact match first (specific component ID)
|
|
345
|
+
if (COMPONENT_CONFIG_MAP[componentType.toLowerCase()]) {
|
|
346
|
+
return COMPONENT_CONFIG_MAP[componentType.toLowerCase()];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Try to find the component's category from snippets
|
|
350
|
+
const snippet = componentSnippets.find(s => s.id === componentType);
|
|
351
|
+
if (snippet) {
|
|
352
|
+
const categoryKey = snippet.category.toLowerCase();
|
|
353
|
+
if (COMPONENT_CONFIG_MAP[categoryKey]) {
|
|
354
|
+
return COMPONENT_CONFIG_MAP[categoryKey];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Default: show all sections if unknown
|
|
359
|
+
return ["colors", "radius", "typography"];
|
|
360
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -555,6 +555,43 @@ function runDevToolsInstaller() {
|
|
|
555
555
|
createdFiles.push(`${pathPrefix}theme/sonance-config.json`);
|
|
556
556
|
console.log(` ✓ Created ${pathPrefix}theme/ with initial files`);
|
|
557
557
|
}
|
|
558
|
+
// 13. Install brand logos to public/logos/
|
|
559
|
+
const publicLogosDir = path.join(targetDir, "public/logos");
|
|
560
|
+
let sourceLogosDir;
|
|
561
|
+
if (IS_BUNDLED) {
|
|
562
|
+
sourceLogosDir = path.join(BUNDLED_ASSETS, "logos");
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
sourceLogosDir = path.join(DEV_PROJECT_ROOT, "public/logos");
|
|
566
|
+
}
|
|
567
|
+
if (fs.existsSync(sourceLogosDir)) {
|
|
568
|
+
// Helper to recursively copy directory
|
|
569
|
+
function copyLogosDir(src, dest, relativePath = "") {
|
|
570
|
+
if (!fs.existsSync(dest)) {
|
|
571
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
574
|
+
for (const entry of entries) {
|
|
575
|
+
const srcPath = path.join(src, entry.name);
|
|
576
|
+
const destPath = path.join(dest, entry.name);
|
|
577
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
578
|
+
if (entry.isDirectory()) {
|
|
579
|
+
copyLogosDir(srcPath, destPath, relPath);
|
|
580
|
+
createdDirectories.push(`public/logos/${relPath}`);
|
|
581
|
+
}
|
|
582
|
+
else if (entry.isFile() && /\.(png|jpg|jpeg|svg|webp)$/i.test(entry.name)) {
|
|
583
|
+
fs.copyFileSync(srcPath, destPath);
|
|
584
|
+
createdFiles.push(`public/logos/${relPath}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
copyLogosDir(sourceLogosDir, publicLogosDir);
|
|
589
|
+
createdDirectories.push("public/logos");
|
|
590
|
+
console.log(` ✓ Installed brand logos to public/logos/`);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
console.log(` ⚠️ Could not find logos source directory`);
|
|
594
|
+
}
|
|
558
595
|
// Detect user's file paths
|
|
559
596
|
const globalsCssPaths = [
|
|
560
597
|
"src/app/globals.css",
|
|
@@ -917,6 +954,237 @@ if (process.argv.includes("uninstall-devtools") || process.argv.includes("--unin
|
|
|
917
954
|
runDevToolsUninstaller();
|
|
918
955
|
process.exit(0);
|
|
919
956
|
}
|
|
957
|
+
// Check for sync command
|
|
958
|
+
if (process.argv.includes("sync")) {
|
|
959
|
+
const fullSync = process.argv.includes("--full");
|
|
960
|
+
runDevToolsSync(fullSync);
|
|
961
|
+
process.exit(0);
|
|
962
|
+
}
|
|
963
|
+
// Check for watch command
|
|
964
|
+
if (process.argv.includes("watch")) {
|
|
965
|
+
runDevToolsWatch();
|
|
966
|
+
// Don't exit - keep watching
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Sync DevTools files to target project without full uninstall/reinstall
|
|
970
|
+
*/
|
|
971
|
+
function runDevToolsSync(fullSync = false) {
|
|
972
|
+
console.log("");
|
|
973
|
+
console.log(" ┌─────────────────────────────────────────────────┐");
|
|
974
|
+
console.log(" │ │");
|
|
975
|
+
console.log(" │ 🔄 Sonance DevTools - Sync │");
|
|
976
|
+
console.log(" │ │");
|
|
977
|
+
console.log(" └─────────────────────────────────────────────────┘");
|
|
978
|
+
console.log("");
|
|
979
|
+
const targetDir = process.cwd();
|
|
980
|
+
const manifestPath = path.join(targetDir, MANIFEST_FILENAME);
|
|
981
|
+
// Check if manifest exists
|
|
982
|
+
if (!fs.existsSync(manifestPath)) {
|
|
983
|
+
console.log(" ❌ No DevTools installation found in this directory.");
|
|
984
|
+
console.log(" Run 'npx sonance-brand-mcp install-devtools' first.");
|
|
985
|
+
console.log("");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
// Read manifest
|
|
989
|
+
let manifest;
|
|
990
|
+
try {
|
|
991
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
console.log(" ❌ Could not read manifest file.");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
console.log(` 📁 Detected structure: ${manifest.structure === "src" ? "src/" : "root"}`);
|
|
998
|
+
console.log(` 📦 Syncing ${fullSync ? "all files" : "core files only"}...`);
|
|
999
|
+
console.log("");
|
|
1000
|
+
const baseDir = manifest.structure === "src" ? "src" : "";
|
|
1001
|
+
let syncedCount = 0;
|
|
1002
|
+
let errorCount = 0;
|
|
1003
|
+
// Determine source paths
|
|
1004
|
+
let sourceDevTools;
|
|
1005
|
+
let sourceBrandSystem;
|
|
1006
|
+
let sourceBrandContext;
|
|
1007
|
+
let sourceApiAnalyze;
|
|
1008
|
+
let sourceApiSaveColors;
|
|
1009
|
+
if (IS_BUNDLED) {
|
|
1010
|
+
sourceDevTools = path.join(BUNDLED_ASSETS, "dev-tools");
|
|
1011
|
+
sourceBrandSystem = path.join(BUNDLED_ASSETS, "brand-system.ts");
|
|
1012
|
+
sourceBrandContext = path.join(BUNDLED_ASSETS, "brand-context.tsx");
|
|
1013
|
+
sourceApiAnalyze = path.join(BUNDLED_ASSETS, "api/sonance-analyze/route.ts");
|
|
1014
|
+
sourceApiSaveColors = path.join(BUNDLED_ASSETS, "api/sonance-save-colors/route.ts");
|
|
1015
|
+
}
|
|
1016
|
+
else {
|
|
1017
|
+
sourceDevTools = path.join(DEV_PROJECT_ROOT, "src/components/dev-tools");
|
|
1018
|
+
sourceBrandSystem = path.join(DEV_PROJECT_ROOT, "src/lib/brand-system.ts");
|
|
1019
|
+
sourceBrandContext = path.join(DEV_PROJECT_ROOT, "src/lib/brand-context.tsx");
|
|
1020
|
+
sourceApiAnalyze = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-analyze/route.ts");
|
|
1021
|
+
sourceApiSaveColors = path.join(DEV_PROJECT_ROOT, "src/app/api/sonance-save-colors/route.ts");
|
|
1022
|
+
}
|
|
1023
|
+
// Core files to sync (always synced)
|
|
1024
|
+
const coreFiles = [
|
|
1025
|
+
{ src: sourceBrandSystem, dest: path.join(targetDir, baseDir, "lib/brand-system.ts"), name: "brand-system.ts" },
|
|
1026
|
+
{ src: sourceBrandContext, dest: path.join(targetDir, baseDir, "lib/brand-context.tsx"), name: "brand-context.tsx" },
|
|
1027
|
+
];
|
|
1028
|
+
// Sync DevTools directory
|
|
1029
|
+
const devToolsDestDir = path.join(targetDir, baseDir, "components/dev-tools");
|
|
1030
|
+
if (fs.existsSync(sourceDevTools) && fs.existsSync(devToolsDestDir)) {
|
|
1031
|
+
const entries = fs.readdirSync(sourceDevTools, { withFileTypes: true });
|
|
1032
|
+
for (const entry of entries) {
|
|
1033
|
+
if (entry.isFile()) {
|
|
1034
|
+
try {
|
|
1035
|
+
fs.copyFileSync(path.join(sourceDevTools, entry.name), path.join(devToolsDestDir, entry.name));
|
|
1036
|
+
console.log(` ✓ Synced components/dev-tools/${entry.name}`);
|
|
1037
|
+
syncedCount++;
|
|
1038
|
+
}
|
|
1039
|
+
catch (err) {
|
|
1040
|
+
console.log(` ✗ Failed: components/dev-tools/${entry.name}`);
|
|
1041
|
+
errorCount++;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
// Sync core lib files
|
|
1047
|
+
for (const file of coreFiles) {
|
|
1048
|
+
if (fs.existsSync(file.src)) {
|
|
1049
|
+
try {
|
|
1050
|
+
fs.copyFileSync(file.src, file.dest);
|
|
1051
|
+
console.log(` ✓ Synced lib/${file.name}`);
|
|
1052
|
+
syncedCount++;
|
|
1053
|
+
}
|
|
1054
|
+
catch (err) {
|
|
1055
|
+
console.log(` ✗ Failed: lib/${file.name}`);
|
|
1056
|
+
errorCount++;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// Full sync includes API routes
|
|
1061
|
+
if (fullSync) {
|
|
1062
|
+
const apiFiles = [
|
|
1063
|
+
{ src: sourceApiAnalyze, dest: path.join(targetDir, baseDir, "app/api/sonance-analyze/route.ts"), name: "sonance-analyze" },
|
|
1064
|
+
];
|
|
1065
|
+
// Check if save-colors exists
|
|
1066
|
+
if (fs.existsSync(sourceApiSaveColors)) {
|
|
1067
|
+
apiFiles.push({ src: sourceApiSaveColors, dest: path.join(targetDir, baseDir, "app/api/sonance-save-colors/route.ts"), name: "sonance-save-colors" });
|
|
1068
|
+
}
|
|
1069
|
+
for (const file of apiFiles) {
|
|
1070
|
+
if (fs.existsSync(file.src)) {
|
|
1071
|
+
try {
|
|
1072
|
+
// Ensure directory exists
|
|
1073
|
+
const destDir = path.dirname(file.dest);
|
|
1074
|
+
if (!fs.existsSync(destDir)) {
|
|
1075
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
1076
|
+
}
|
|
1077
|
+
fs.copyFileSync(file.src, file.dest);
|
|
1078
|
+
console.log(` ✓ Synced api/${file.name}/route.ts`);
|
|
1079
|
+
syncedCount++;
|
|
1080
|
+
}
|
|
1081
|
+
catch (err) {
|
|
1082
|
+
console.log(` ✗ Failed: api/${file.name}/route.ts`);
|
|
1083
|
+
errorCount++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
console.log("");
|
|
1089
|
+
if (errorCount === 0) {
|
|
1090
|
+
console.log(` ✅ Synced ${syncedCount} file(s) successfully!`);
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
console.log(` ⚠️ Synced ${syncedCount} file(s), ${errorCount} failed.`);
|
|
1094
|
+
}
|
|
1095
|
+
console.log("");
|
|
1096
|
+
console.log(" 💡 Tip: Restart your dev server to pick up changes.");
|
|
1097
|
+
console.log("");
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Watch DevTools source files and auto-sync on changes
|
|
1101
|
+
*/
|
|
1102
|
+
function runDevToolsWatch() {
|
|
1103
|
+
console.log("");
|
|
1104
|
+
console.log(" ┌─────────────────────────────────────────────────┐");
|
|
1105
|
+
console.log(" │ │");
|
|
1106
|
+
console.log(" │ 👁️ Sonance DevTools - Watch Mode │");
|
|
1107
|
+
console.log(" │ │");
|
|
1108
|
+
console.log(" └─────────────────────────────────────────────────┘");
|
|
1109
|
+
console.log("");
|
|
1110
|
+
const targetDir = process.cwd();
|
|
1111
|
+
const manifestPath = path.join(targetDir, MANIFEST_FILENAME);
|
|
1112
|
+
// Check if manifest exists
|
|
1113
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1114
|
+
console.log(" ❌ No DevTools installation found in this directory.");
|
|
1115
|
+
console.log(" Run 'npx sonance-brand-mcp install-devtools' first.");
|
|
1116
|
+
console.log("");
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1119
|
+
// Read manifest
|
|
1120
|
+
let manifest;
|
|
1121
|
+
try {
|
|
1122
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1123
|
+
}
|
|
1124
|
+
catch {
|
|
1125
|
+
console.log(" ❌ Could not read manifest file.");
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
}
|
|
1128
|
+
console.log(` 📁 Structure: ${manifest.structure === "src" ? "src/" : "root"}`);
|
|
1129
|
+
console.log(" 👀 Watching for changes...");
|
|
1130
|
+
console.log(" 📝 Press Ctrl+C to stop");
|
|
1131
|
+
console.log("");
|
|
1132
|
+
// Determine what to watch based on environment
|
|
1133
|
+
let watchDir;
|
|
1134
|
+
if (IS_BUNDLED) {
|
|
1135
|
+
console.log(" ⚠️ Watch mode works best in development.");
|
|
1136
|
+
console.log(" For bundled version, use 'sync' command instead.");
|
|
1137
|
+
console.log("");
|
|
1138
|
+
process.exit(0);
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
watchDir = path.join(DEV_PROJECT_ROOT, "src");
|
|
1142
|
+
}
|
|
1143
|
+
// Set up file watcher
|
|
1144
|
+
const watchedPaths = [
|
|
1145
|
+
path.join(watchDir, "components/dev-tools"),
|
|
1146
|
+
path.join(watchDir, "lib/brand-system.ts"),
|
|
1147
|
+
path.join(watchDir, "lib/brand-context.tsx"),
|
|
1148
|
+
path.join(watchDir, "app/api/sonance-analyze"),
|
|
1149
|
+
];
|
|
1150
|
+
let debounceTimer = null;
|
|
1151
|
+
const handleChange = (eventType, filename) => {
|
|
1152
|
+
// Debounce rapid changes
|
|
1153
|
+
if (debounceTimer)
|
|
1154
|
+
clearTimeout(debounceTimer);
|
|
1155
|
+
debounceTimer = setTimeout(() => {
|
|
1156
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
1157
|
+
console.log(` [${timestamp}] 🔄 Change detected${filename ? `: ${filename}` : ""}`);
|
|
1158
|
+
runDevToolsSync(false);
|
|
1159
|
+
}, 300);
|
|
1160
|
+
};
|
|
1161
|
+
// Watch each path
|
|
1162
|
+
for (const watchPath of watchedPaths) {
|
|
1163
|
+
if (fs.existsSync(watchPath)) {
|
|
1164
|
+
try {
|
|
1165
|
+
const stat = fs.statSync(watchPath);
|
|
1166
|
+
if (stat.isDirectory()) {
|
|
1167
|
+
fs.watch(watchPath, { recursive: true }, handleChange);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
fs.watch(watchPath, handleChange);
|
|
1171
|
+
}
|
|
1172
|
+
console.log(` 📂 Watching: ${path.relative(DEV_PROJECT_ROOT, watchPath)}`);
|
|
1173
|
+
}
|
|
1174
|
+
catch (err) {
|
|
1175
|
+
console.log(` ⚠️ Could not watch: ${watchPath}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
console.log("");
|
|
1180
|
+
// Keep process alive
|
|
1181
|
+
process.on("SIGINT", () => {
|
|
1182
|
+
console.log("");
|
|
1183
|
+
console.log(" 👋 Watch mode stopped.");
|
|
1184
|
+
console.log("");
|
|
1185
|
+
process.exit(0);
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
920
1188
|
// Resolve paths based on environment
|
|
921
1189
|
function getAssetPath(assetType) {
|
|
922
1190
|
if (IS_BUNDLED) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonance-brand-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.17",
|
|
4
4
|
"description": "MCP Server for Sonance Brand Guidelines and Component Library - gives Claude instant access to brand colors, typography, and UI components.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|