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.
- package/dist/assets/api/sonance-analyze/route.ts +156 -16
- package/dist/index.js +133 -44
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
320
|
-
if (category === "
|
|
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 "
|
|
426
|
-
// Add
|
|
427
|
-
|
|
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
|
-
//
|
|
430
|
-
if (
|
|
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
|
|
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(
|
|
668
|
-
relativePath.startsWith(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
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
|
-
//
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
//
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
788
|
-
|
|
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.
|
|
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",
|