sonance-brand-mcp 1.3.12 → 1.3.14

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.
@@ -51,6 +51,9 @@ interface ScannedElement {
51
51
  textContent?: string; // Truncated text content (for headings/paragraphs)
52
52
  // Component-specific fields
53
53
  variant?: string; // variant prop if present
54
+ // Interactive element fields (Button, Link, etc.)
55
+ onClickHandler?: string; // onClick handler name (e.g., "handleReset")
56
+ iconName?: string; // Icon component inside (e.g., "RotateCcw")
54
57
  // Input-specific fields
55
58
  inputName?: string; // name or placeholder if present
56
59
  inputType?: string; // type attribute if present
@@ -140,6 +143,27 @@ const ELEMENT_CATEGORIES: Record<string, ElementCategory> = {
140
143
  // All element tags to scan for
141
144
  const ALL_ELEMENT_TAGS = Object.keys(ELEMENT_CATEGORIES);
142
145
 
146
+ /**
147
+ * Detect project structure by checking where layout.tsx lives
148
+ */
149
+ function detectProjectStructure(projectRoot: string): { baseDir: string; useSrcDir: boolean } {
150
+ const hasRootLayout = fs.existsSync(path.join(projectRoot, "app/layout.tsx"));
151
+ const hasSrcLayout = fs.existsSync(path.join(projectRoot, "src/app/layout.tsx"));
152
+
153
+ let useSrcDir: boolean;
154
+ if (hasRootLayout) {
155
+ useSrcDir = false;
156
+ } else if (hasSrcLayout) {
157
+ useSrcDir = true;
158
+ } else {
159
+ // Fallback to directory detection
160
+ useSrcDir = fs.existsSync(path.join(projectRoot, "src/app")) ||
161
+ fs.existsSync(path.join(projectRoot, "src/components"));
162
+ }
163
+
164
+ return { baseDir: useSrcDir ? "src" : "", useSrcDir };
165
+ }
166
+
143
167
  /**
144
168
  * Extracts all design-related elements from a file using pattern matching.
145
169
  * This is a simplified AST approach that handles common patterns.
@@ -316,13 +340,38 @@ function extractElements(
316
340
  }
317
341
  }
318
342
 
319
- // Component-specific extraction
320
- if (category === "component") {
343
+ // Interactive element extraction (Button, Link, etc.)
344
+ if (category === "interactive") {
321
345
  // Extract variant
322
346
  const variantMatch = elementContent.match(/variant=["']([^"']+)["']/);
323
347
  if (variantMatch) {
324
348
  element.variant = variantMatch[1];
325
349
  }
350
+
351
+ // Extract onClick handler name
352
+ const onClickMatch = elementContent.match(/onClick=\{([a-zA-Z_][a-zA-Z0-9_]*)\}/);
353
+ if (onClickMatch) {
354
+ element.onClickHandler = onClickMatch[1];
355
+ }
356
+
357
+ // Extract icon name (first PascalCase component inside the element)
358
+ const iconMatch = elementContent.match(/<([A-Z][a-zA-Z0-9]*)\s/);
359
+ if (iconMatch && iconMatch[1] !== elementType) {
360
+ element.iconName = iconMatch[1];
361
+ }
362
+
363
+ // Extract text content between > and </ElementType>
364
+ const afterOpenTag = fullContent.substring(elementEnd);
365
+ const closeTagMatch = afterOpenTag.match(new RegExp(`^([^<]*)<\\/${elementType}>`));
366
+ if (closeTagMatch && closeTagMatch[1].trim()) {
367
+ element.textContent = closeTagMatch[1].trim().substring(0, 50);
368
+ } else {
369
+ // Try to find text after any icons/nested elements
370
+ const textAfterIcons = afterOpenTag.match(new RegExp(`^(?:<[^>]+>\\s*)*([^<]+)<\\/${elementType}>`));
371
+ if (textAfterIcons && textAfterIcons[1].trim()) {
372
+ element.textContent = textAfterIcons[1].trim().substring(0, 50);
373
+ }
374
+ }
326
375
  }
327
376
 
328
377
  // Input-specific extraction
@@ -422,15 +471,49 @@ function generateSuggestedId(element: ScannedElement): string {
422
471
  break;
423
472
  }
424
473
 
425
- case "component": {
426
- // Add element type (button, link, etc.)
427
- parts.push(elementType.toLowerCase());
474
+ case "interactive": {
475
+ // Add prefix based on element type
476
+ const prefix = elementType === "Button" ? "btn" :
477
+ elementType === "Link" || elementType === "a" ? "link" : "action";
478
+ parts.push(prefix);
428
479
 
429
- // Add variant if present
430
- if (variant) {
480
+ // Priority 1: Use text content if available
481
+ if (textContent) {
482
+ const cleanText = textContent.toLowerCase()
483
+ .replace(/[^a-z0-9\s]/g, "")
484
+ .replace(/\s+/g, "-")
485
+ .substring(0, 25);
486
+ if (cleanText && cleanText.length > 1) {
487
+ parts.push(cleanText);
488
+ }
489
+ }
490
+
491
+ // Priority 2: Use onClick handler name
492
+ if (parts.length === 1 && element.onClickHandler) {
493
+ // Convert camelCase to kebab-case (handleReset -> handle-reset)
494
+ const handlerName = element.onClickHandler
495
+ .replace(/^(handle|on)/i, "") // Remove handle/on prefix
496
+ .replace(/([A-Z])/g, "-$1")
497
+ .toLowerCase()
498
+ .replace(/^-/, "");
499
+ if (handlerName) parts.push(handlerName);
500
+ }
501
+
502
+ // Priority 3: Use icon name if no text
503
+ if (parts.length === 1 && element.iconName) {
504
+ const iconName = element.iconName
505
+ .replace(/([A-Z])/g, "-$1")
506
+ .toLowerCase()
507
+ .replace(/^-/, "");
508
+ parts.push(iconName);
509
+ }
510
+
511
+ // Priority 4: Use variant
512
+ if (parts.length === 1 && variant) {
431
513
  parts.push(variant.toLowerCase());
432
514
  }
433
515
 
516
+ // Fallback: add "action" if still only prefix
434
517
  if (parts.length === 1) parts.push("action");
435
518
  break;
436
519
  }
@@ -622,6 +705,7 @@ function analyzeColorArchitecture(sources: ColorSource[]): ColorArchitecture {
622
705
  function scanDirectory(
623
706
  dir: string,
624
707
  projectRoot: string,
708
+ baseDir: string = "src",
625
709
  extensions: string[] = [".tsx", ".jsx", ".js", ".ts", ".css"]
626
710
  ): { elements: ScannedElement[]; images: ScannedElement[]; themeFiles: ThemeFile[]; colorSources: ColorSource[]; filesScanned: number } {
627
711
  const elements: ScannedElement[] = [];
@@ -661,11 +745,13 @@ function scanDirectory(
661
745
  elements.push(...fileElements);
662
746
  }
663
747
 
664
- // Detect component definitions (files in src/components/ui or src/components/layout)
748
+ // Detect component definitions (files in components/ui or components/layout)
665
749
  if (entry.name.endsWith(".tsx") && !entry.name.endsWith(".stories.tsx")) {
750
+ const componentsUiPath = baseDir ? `${baseDir}/components/ui/` : "components/ui/";
751
+ const componentsLayoutPath = baseDir ? `${baseDir}/components/layout/` : "components/layout/";
666
752
  const isComponentDef =
667
- relativePath.startsWith("src/components/ui/") ||
668
- relativePath.startsWith("src/components/layout/");
753
+ relativePath.startsWith(componentsUiPath) ||
754
+ relativePath.startsWith(componentsLayoutPath);
669
755
 
670
756
  if (isComponentDef) {
671
757
  const componentName = entry.name.replace(".tsx", "");
@@ -723,7 +809,23 @@ function scanDirectory(
723
809
  * Tags a component definition file with data-sonance-name attribute.
724
810
  * Uses multiple regex patterns to handle different component structures.
725
811
  */
726
- function tagComponentDefinition(filePath: string, content: string, componentName: string): { success: boolean; content: string; error?: string } {
812
+ function tagComponentDefinition(filePath: string, content: string, componentName: string): { success: boolean; content: string; error?: string; skipped?: boolean } {
813
+ // Check for pure re-exports (no JSX to tag)
814
+ // Pattern: const ComponentName = SomePrimitive.Element or export const X = Y
815
+ const pureReExportPattern = new RegExp(
816
+ `(?:const|export\\s+const)\\s+${componentName}\\s*=\\s*[A-Z][a-zA-Z0-9]*(?:\\.[A-Z][a-zA-Z0-9]*)?\\s*[;\\n]`,
817
+ 'g'
818
+ );
819
+ if (pureReExportPattern.test(content) && !content.includes(`<`) ||
820
+ (content.match(/<[A-Z]/g) || []).length === 0) {
821
+ return {
822
+ success: true,
823
+ content,
824
+ skipped: true,
825
+ error: "Pure re-export - no JSX to tag"
826
+ };
827
+ }
828
+
727
829
  // Helper to inject tag only if not present in the match
728
830
  const injectTag = (match: string, p1: string, p2: string) => {
729
831
  if (match.includes("data-sonance-name=")) return match;
@@ -816,6 +918,39 @@ function tagComponentDefinition(filePath: string, content: string, componentName
816
918
  }
817
919
  }
818
920
 
921
+ // Pattern 8: forwardRef with implicit return and element without attributes (needs to add space)
922
+ // Matches: ) => (\n <Element.Sub>\n or ) => (\n <Element>\n
923
+ if (!modified) {
924
+ const forwardRefNoAttrsPattern = /(\)\s*=>\s*\(\s*<(?:[A-Z][a-zA-Z0-9]*\.)?[A-Z][a-zA-Z0-9]*)(?![^>]*data-sonance-name=)(>)/;
925
+ if (forwardRefNoAttrsPattern.test(result)) {
926
+ // Different injection - we need to add space before the >
927
+ const newContent = result.replace(forwardRefNoAttrsPattern, (match, p1, p2) => {
928
+ if (match.includes("data-sonance-name=")) return match;
929
+ return `${p1} data-sonance-name="${componentName}"${p2}`;
930
+ });
931
+ if (newContent !== result) {
932
+ result = newContent;
933
+ modified = true;
934
+ }
935
+ }
936
+ }
937
+
938
+ // Pattern 9: Standard function component returning element without attributes
939
+ // Matches: return (\n <DropdownMenu>\n
940
+ if (!modified) {
941
+ const returnNoAttrsPattern = /(return\s*\(\s*<(?:[A-Z][a-zA-Z0-9]*\.)?[A-Z][a-zA-Z0-9]*)(?![^>]*data-sonance-name=)(>)/;
942
+ if (returnNoAttrsPattern.test(result)) {
943
+ const newContent = result.replace(returnNoAttrsPattern, (match, p1, p2) => {
944
+ if (match.includes("data-sonance-name=")) return match;
945
+ return `${p1} data-sonance-name="${componentName}"${p2}`;
946
+ });
947
+ if (newContent !== result) {
948
+ result = newContent;
949
+ modified = true;
950
+ }
951
+ }
952
+ }
953
+
819
954
  if (modified) {
820
955
  return { success: true, content: result };
821
956
  } else {
@@ -838,10 +973,11 @@ export async function GET() {
838
973
 
839
974
  try {
840
975
  const projectRoot = process.cwd();
841
- const srcDir = path.join(projectRoot, "src");
976
+ const { baseDir } = detectProjectStructure(projectRoot);
977
+ const scanRoot = baseDir ? path.join(projectRoot, baseDir) : projectRoot;
842
978
 
843
979
  // Scan the codebase
844
- const { elements, images, themeFiles, colorSources, filesScanned } = scanDirectory(srcDir, projectRoot);
980
+ const { elements, images, themeFiles, colorSources, filesScanned } = scanDirectory(scanRoot, projectRoot, baseDir);
845
981
 
846
982
  // Analyze color architecture
847
983
  const colorArchitecture = analyzeColorArchitecture(colorSources);
@@ -920,10 +1056,11 @@ export async function POST(request: Request) {
920
1056
  if (action === "auto-tag-all") {
921
1057
  // Bulk inject IDs into elements missing them
922
1058
  const projectRoot = process.cwd();
923
- const srcDir = path.join(projectRoot, "src");
1059
+ const { baseDir } = detectProjectStructure(projectRoot);
1060
+ const scanRoot = baseDir ? path.join(projectRoot, baseDir) : projectRoot;
924
1061
 
925
1062
  // Re-scan to get fresh data
926
- const { elements } = scanDirectory(srcDir, projectRoot);
1063
+ const { elements } = scanDirectory(scanRoot, projectRoot, baseDir);
927
1064
 
928
1065
  // Filter to elements that need IDs
929
1066
  let targetElements = elements.filter(el => !el.hasId);
@@ -974,7 +1111,10 @@ export async function POST(request: Request) {
974
1111
  const componentName = element.context.parentComponent || element.suggestedId;
975
1112
  const tagResult = tagComponentDefinition(fullPath, content, componentName);
976
1113
 
977
- if (tagResult.success) {
1114
+ if (tagResult.skipped) {
1115
+ // Pure re-export - no JSX to tag, mark as skipped (not a failure)
1116
+ results.push({ id: element.id, success: true, skipped: true });
1117
+ } else if (tagResult.success) {
978
1118
  fs.writeFileSync(fullPath, tagResult.content, "utf-8");
979
1119
  results.push({ id: element.id, success: true });
980
1120
  } else {
package/dist/index.js CHANGED
@@ -27,6 +27,8 @@ function getClaudeConfigPath() {
27
27
  return path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
28
28
  }
29
29
  }
30
+ // ---- DevTools Installation Manifest ----
31
+ const MANIFEST_FILENAME = ".sonance-devtools-manifest.json";
30
32
  /**
31
33
  * Run the installer to add sonance-brand to Claude Desktop config
32
34
  */
@@ -430,25 +432,34 @@ function runDevToolsInstaller() {
430
432
  console.log(" 📂 Installing files...");
431
433
  // Path prefix for logging (shows "src/" or "" based on detected structure)
432
434
  const pathPrefix = baseDir ? `${baseDir}/` : "";
435
+ // Track all created files and directories for the manifest
436
+ const createdFiles = [];
437
+ const createdDirectories = [];
438
+ const modifiedFiles = [];
433
439
  // 1. Install lib files (brand-system.ts, brand-context.tsx, utils.ts)
434
440
  if (!fs.existsSync(libDir)) {
435
441
  fs.mkdirSync(libDir, { recursive: true });
436
442
  }
437
443
  fs.copyFileSync(sourceBrandSystem, path.join(libDir, "brand-system.ts"));
444
+ createdFiles.push(`${pathPrefix}lib/brand-system.ts`);
438
445
  console.log(` ✓ Created ${pathPrefix}lib/brand-system.ts`);
439
446
  fs.copyFileSync(sourceBrandContext, path.join(libDir, "brand-context.tsx"));
447
+ createdFiles.push(`${pathPrefix}lib/brand-context.tsx`);
440
448
  console.log(` ✓ Created ${pathPrefix}lib/brand-context.tsx`);
441
449
  fs.copyFileSync(sourceUtils, path.join(libDir, "utils.ts"));
450
+ createdFiles.push(`${pathPrefix}lib/utils.ts`);
442
451
  console.log(` ✓ Created ${pathPrefix}lib/utils.ts`);
443
452
  // 2. Install DevTools components
444
453
  if (!fs.existsSync(devToolsDir)) {
445
454
  fs.mkdirSync(devToolsDir, { recursive: true });
446
455
  }
456
+ createdDirectories.push(`${pathPrefix}components/dev-tools`);
447
457
  // Copy directory contents
448
458
  const entries = fs.readdirSync(sourceDevTools, { withFileTypes: true });
449
459
  for (const entry of entries) {
450
460
  if (entry.isFile()) {
451
461
  fs.copyFileSync(path.join(sourceDevTools, entry.name), path.join(devToolsDir, entry.name));
462
+ createdFiles.push(`${pathPrefix}components/dev-tools/${entry.name}`);
452
463
  }
453
464
  }
454
465
  console.log(` ✓ Created ${pathPrefix}components/dev-tools/`);
@@ -456,47 +467,61 @@ function runDevToolsInstaller() {
456
467
  if (!fs.existsSync(apiThemeDir)) {
457
468
  fs.mkdirSync(apiThemeDir, { recursive: true });
458
469
  }
470
+ createdDirectories.push(`${pathPrefix}app/api/sonance-theme`);
459
471
  fs.copyFileSync(sourceApiTheme, path.join(apiThemeDir, "route.ts"));
472
+ createdFiles.push(`${pathPrefix}app/api/sonance-theme/route.ts`);
460
473
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-theme/route.ts`);
461
474
  // 4. Install API route for component detection
462
475
  if (!fs.existsSync(apiComponentsDir)) {
463
476
  fs.mkdirSync(apiComponentsDir, { recursive: true });
464
477
  }
478
+ createdDirectories.push(`${pathPrefix}app/api/sonance-components`);
465
479
  fs.copyFileSync(sourceApiComponents, path.join(apiComponentsDir, "route.ts"));
480
+ createdFiles.push(`${pathPrefix}app/api/sonance-components/route.ts`);
466
481
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-components/route.ts`);
467
482
  // 5. Install API route for saving logo configuration
468
483
  if (!fs.existsSync(apiSaveLogoDir)) {
469
484
  fs.mkdirSync(apiSaveLogoDir, { recursive: true });
470
485
  }
486
+ createdDirectories.push(`${pathPrefix}app/api/sonance-save-logo`);
471
487
  fs.copyFileSync(sourceApiSaveLogo, path.join(apiSaveLogoDir, "route.ts"));
488
+ createdFiles.push(`${pathPrefix}app/api/sonance-save-logo/route.ts`);
472
489
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-save-logo/route.ts`);
473
490
  // 7. Install API route for listing logo assets
474
491
  if (!fs.existsSync(apiAssetsDir)) {
475
492
  fs.mkdirSync(apiAssetsDir, { recursive: true });
476
493
  }
494
+ createdDirectories.push(`${pathPrefix}app/api/sonance-assets`);
477
495
  fs.copyFileSync(sourceApiAssets, path.join(apiAssetsDir, "route.ts"));
496
+ createdFiles.push(`${pathPrefix}app/api/sonance-assets/route.ts`);
478
497
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-assets/route.ts`);
479
498
  // 8. Install API route for auto-fixing logo IDs
480
499
  if (!fs.existsSync(apiInjectIdDir)) {
481
500
  fs.mkdirSync(apiInjectIdDir, { recursive: true });
482
501
  }
502
+ createdDirectories.push(`${pathPrefix}app/api/sonance-inject-id`);
483
503
  fs.copyFileSync(sourceApiInjectId, path.join(apiInjectIdDir, "route.ts"));
504
+ createdFiles.push(`${pathPrefix}app/api/sonance-inject-id/route.ts`);
484
505
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-inject-id/route.ts`);
485
506
  // 9. Install API route for project analysis
486
507
  if (!fs.existsSync(apiAnalyzeDir)) {
487
508
  fs.mkdirSync(apiAnalyzeDir, { recursive: true });
488
509
  }
510
+ createdDirectories.push(`${pathPrefix}app/api/sonance-analyze`);
489
511
  fs.copyFileSync(sourceApiAnalyze, path.join(apiAnalyzeDir, "route.ts"));
512
+ createdFiles.push(`${pathPrefix}app/api/sonance-analyze/route.ts`);
490
513
  console.log(` ✓ Created ${pathPrefix}app/api/sonance-analyze/route.ts`);
491
514
  // 11. Install brand-overrides.css for production logo sizing
492
515
  if (!fs.existsSync(stylesDir)) {
493
516
  fs.mkdirSync(stylesDir, { recursive: true });
494
517
  }
495
518
  fs.copyFileSync(sourceBrandOverridesCss, path.join(stylesDir, "brand-overrides.css"));
519
+ createdFiles.push(`${pathPrefix}styles/brand-overrides.css`);
496
520
  console.log(` ✓ Created ${pathPrefix}styles/brand-overrides.css`);
497
521
  // 12. Create theme directory with initial files
498
522
  if (!fs.existsSync(themeDir)) {
499
523
  fs.mkdirSync(themeDir, { recursive: true });
524
+ createdDirectories.push(`${pathPrefix}theme`);
500
525
  // Create initial theme CSS
501
526
  const initialCss = `/**
502
527
  * Sonance Theme - Auto-generated by DevTools
@@ -508,6 +533,7 @@ function runDevToolsInstaller() {
508
533
  }
509
534
  `;
510
535
  fs.writeFileSync(path.join(themeDir, "sonance-theme.css"), initialCss, "utf-8");
536
+ createdFiles.push(`${pathPrefix}theme/sonance-theme.css`);
511
537
  // Create initial config
512
538
  const initialConfig = {
513
539
  baseColor: "#333F48",
@@ -519,6 +545,7 @@ function runDevToolsInstaller() {
519
545
  spacing: "default"
520
546
  };
521
547
  fs.writeFileSync(path.join(themeDir, "sonance-config.json"), JSON.stringify(initialConfig, null, 2), "utf-8");
548
+ createdFiles.push(`${pathPrefix}theme/sonance-config.json`);
522
549
  console.log(` ✓ Created ${pathPrefix}theme/ with initial files`);
523
550
  }
524
551
  // Detect user's file paths
@@ -571,6 +598,7 @@ function runDevToolsInstaller() {
571
598
  if (newImports.length > 0) {
572
599
  cssContent = newImports.join("\n") + "\n" + cssContent;
573
600
  fs.writeFileSync(globalsFullPath, cssContent, "utf-8");
601
+ modifiedFiles.push({ path: detectedGlobalsCss, modification: "css-import" });
574
602
  }
575
603
  cssAutoConfigured = true;
576
604
  }
@@ -626,6 +654,7 @@ function runDevToolsInstaller() {
626
654
  }
627
655
  if (modified) {
628
656
  fs.writeFileSync(layoutFullPath, layoutContent, "utf-8");
657
+ modifiedFiles.push({ path: detectedLayout, modification: "layout-component" });
629
658
  }
630
659
  // Check if layout was fully configured
631
660
  layoutAutoConfigured = layoutContent.includes("SonanceDevTools") && layoutContent.includes("<SonanceDevTools");
@@ -664,6 +693,18 @@ function runDevToolsInstaller() {
664
693
  console.log(" 🚀 DONE! Run your dev server and look for the DevTools button.");
665
694
  }
666
695
  console.log("");
696
+ // Write installation manifest for safe uninstall
697
+ const manifest = {
698
+ version: "1.3.12",
699
+ installedAt: new Date().toISOString(),
700
+ structure: useSrcDir ? "src" : "root",
701
+ files: createdFiles,
702
+ directories: createdDirectories,
703
+ modifiedFiles: modifiedFiles
704
+ };
705
+ fs.writeFileSync(path.join(targetDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2));
706
+ console.log(` 📝 Created ${MANIFEST_FILENAME} (for safe uninstall)`);
707
+ console.log("");
667
708
  }
668
709
  /**
669
710
  * Run the uninstaller for DevTools Plugin (removes files and imports)
@@ -743,54 +784,102 @@ function runDevToolsUninstaller() {
743
784
  }
744
785
  }
745
786
  // --- 3. Delete installed directories/files ---
746
- // Detect project structure by checking where layout.tsx actually lives
747
- const hasRootLayout = fs.existsSync(path.join(targetDir, "app/layout.tsx"));
748
- const hasSrcLayout = fs.existsSync(path.join(targetDir, "src/app/layout.tsx"));
749
- let useSrcDir;
750
- if (hasRootLayout) {
751
- // Root layout exists - use root structure (even if src/ also exists)
752
- useSrcDir = false;
753
- }
754
- else if (hasSrcLayout) {
755
- // Only src layout exists
756
- useSrcDir = true;
787
+ // Check for manifest file first (safe uninstall)
788
+ const manifestPath = path.join(targetDir, MANIFEST_FILENAME);
789
+ if (fs.existsSync(manifestPath)) {
790
+ // Use manifest for safe, precise uninstall
791
+ console.log(" 📋 Found installation manifest - using safe uninstall");
792
+ console.log("");
793
+ try {
794
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
795
+ // Delete files listed in manifest
796
+ for (const file of manifest.files) {
797
+ const fullPath = path.join(targetDir, file);
798
+ if (fs.existsSync(fullPath)) {
799
+ try {
800
+ fs.unlinkSync(fullPath);
801
+ removedItems.push(`Deleted ${file}`);
802
+ }
803
+ catch (err) {
804
+ errors.push(`Could not delete ${file}`);
805
+ }
806
+ }
807
+ }
808
+ // Delete directories listed in manifest (in reverse order to handle nested)
809
+ const sortedDirs = [...manifest.directories].sort((a, b) => b.length - a.length);
810
+ for (const dir of sortedDirs) {
811
+ const fullPath = path.join(targetDir, dir);
812
+ if (fs.existsSync(fullPath)) {
813
+ try {
814
+ fs.rmSync(fullPath, { recursive: true, force: true });
815
+ removedItems.push(`Deleted ${dir}/`);
816
+ }
817
+ catch (err) {
818
+ errors.push(`Could not delete ${dir}`);
819
+ }
820
+ }
821
+ }
822
+ // Delete the manifest file itself
823
+ fs.unlinkSync(manifestPath);
824
+ removedItems.push(`Deleted ${MANIFEST_FILENAME}`);
825
+ }
826
+ catch (err) {
827
+ errors.push("Could not parse manifest file - falling back to legacy uninstall");
828
+ }
757
829
  }
758
830
  else {
759
- // No layout found - fall back to directory detection
760
- useSrcDir = fs.existsSync(path.join(targetDir, "src/app")) ||
761
- fs.existsSync(path.join(targetDir, "src/components"));
762
- }
763
- const baseDir = useSrcDir ? "src" : "";
764
- const pathPrefix = baseDir ? `${baseDir}/` : "";
765
- const itemsToDelete = [
766
- `${pathPrefix}components/dev-tools`,
767
- `${pathPrefix}styles/brand-overrides.css`,
768
- `${pathPrefix}theme`,
769
- `${pathPrefix}app/api/sonance-theme`,
770
- `${pathPrefix}app/api/sonance-components`,
771
- `${pathPrefix}app/api/sonance-save-logo`,
772
- `${pathPrefix}app/api/sonance-assets`,
773
- `${pathPrefix}app/api/sonance-inject-id`,
774
- `${pathPrefix}app/api/sonance-analyze`,
775
- `${pathPrefix}lib/brand-system.ts`,
776
- `${pathPrefix}lib/brand-context.tsx`,
777
- `${pathPrefix}lib/utils.ts`
778
- ];
779
- for (const item of itemsToDelete) {
780
- const fullPath = path.join(targetDir, item);
781
- if (fs.existsSync(fullPath)) {
782
- try {
783
- const stats = fs.statSync(fullPath);
784
- if (stats.isDirectory()) {
785
- fs.rmSync(fullPath, { recursive: true, force: true });
831
+ // Legacy uninstall (no manifest) - warn user and use hardcoded list
832
+ console.log(" ⚠️ No installation manifest found - using legacy uninstall");
833
+ console.log(" (This may delete files that existed before installation)");
834
+ console.log("");
835
+ // Detect project structure by checking where layout.tsx actually lives
836
+ const hasRootLayout = fs.existsSync(path.join(targetDir, "app/layout.tsx"));
837
+ const hasSrcLayout = fs.existsSync(path.join(targetDir, "src/app/layout.tsx"));
838
+ let useSrcDir;
839
+ if (hasRootLayout) {
840
+ useSrcDir = false;
841
+ }
842
+ else if (hasSrcLayout) {
843
+ useSrcDir = true;
844
+ }
845
+ else {
846
+ useSrcDir = fs.existsSync(path.join(targetDir, "src/app")) ||
847
+ fs.existsSync(path.join(targetDir, "src/components"));
848
+ }
849
+ const baseDir = useSrcDir ? "src" : "";
850
+ const pathPrefix = baseDir ? `${baseDir}/` : "";
851
+ // Legacy list - NOTE: Excludes lib/utils.ts to prevent deleting user's pre-existing utils
852
+ const itemsToDelete = [
853
+ `${pathPrefix}components/dev-tools`,
854
+ `${pathPrefix}styles/brand-overrides.css`,
855
+ `${pathPrefix}theme`,
856
+ `${pathPrefix}app/api/sonance-theme`,
857
+ `${pathPrefix}app/api/sonance-components`,
858
+ `${pathPrefix}app/api/sonance-save-logo`,
859
+ `${pathPrefix}app/api/sonance-assets`,
860
+ `${pathPrefix}app/api/sonance-inject-id`,
861
+ `${pathPrefix}app/api/sonance-analyze`,
862
+ `${pathPrefix}lib/brand-system.ts`,
863
+ `${pathPrefix}lib/brand-context.tsx`
864
+ // NOTE: lib/utils.ts intentionally excluded from legacy uninstall
865
+ // to prevent deleting pre-existing user files
866
+ ];
867
+ for (const item of itemsToDelete) {
868
+ const fullPath = path.join(targetDir, item);
869
+ if (fs.existsSync(fullPath)) {
870
+ try {
871
+ const stats = fs.statSync(fullPath);
872
+ if (stats.isDirectory()) {
873
+ fs.rmSync(fullPath, { recursive: true, force: true });
874
+ }
875
+ else {
876
+ fs.unlinkSync(fullPath);
877
+ }
878
+ removedItems.push(`Deleted ${item}`);
786
879
  }
787
- else {
788
- fs.unlinkSync(fullPath);
880
+ catch (err) {
881
+ errors.push(`Could not delete ${item}`);
789
882
  }
790
- removedItems.push(`Deleted ${item}`);
791
- }
792
- catch (err) {
793
- errors.push(`Could not delete ${item}`);
794
883
  }
795
884
  }
796
885
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sonance-brand-mcp",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
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",