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.
@@ -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.15",
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",