sonance-brand-mcp 1.3.19 → 1.3.20

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,174 @@
1
+ import { NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ /**
6
+ * Sonance DevTools API - Dynamic Component File Discovery
7
+ *
8
+ * Searches the filesystem to find the source file for a given component type.
9
+ * This enables the AI editor to work with any project structure.
10
+ *
11
+ * DEVELOPMENT ONLY.
12
+ */
13
+
14
+ // Common component directory patterns (in priority order)
15
+ const SEARCH_PATTERNS = [
16
+ "src/components/ui",
17
+ "src/components",
18
+ "components/ui",
19
+ "components",
20
+ "app/components/ui",
21
+ "app/components",
22
+ "src/app/components",
23
+ ];
24
+
25
+ // File extensions to try
26
+ const EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
27
+
28
+ /**
29
+ * Recursively search for a file matching the component name
30
+ */
31
+ function findFileRecursive(
32
+ dir: string,
33
+ fileName: string,
34
+ maxDepth: number = 3,
35
+ currentDepth: number = 0
36
+ ): string | null {
37
+ if (currentDepth > maxDepth) return null;
38
+
39
+ try {
40
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
41
+
42
+ // First, check for direct matches in this directory
43
+ for (const ext of EXTENSIONS) {
44
+ const targetFile = `${fileName}${ext}`;
45
+ const match = entries.find(
46
+ (e) => e.isFile() && e.name.toLowerCase() === targetFile.toLowerCase()
47
+ );
48
+ if (match) {
49
+ return path.join(dir, match.name);
50
+ }
51
+ }
52
+
53
+ // Then recurse into subdirectories
54
+ for (const entry of entries) {
55
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
56
+ const result = findFileRecursive(
57
+ path.join(dir, entry.name),
58
+ fileName,
59
+ maxDepth,
60
+ currentDepth + 1
61
+ );
62
+ if (result) return result;
63
+ }
64
+ }
65
+ } catch {
66
+ // Directory not readable, skip
67
+ }
68
+
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * Try to find a component file using various strategies
74
+ */
75
+ function findComponentFile(projectRoot: string, componentType: string): string | null {
76
+ // Normalize component type (e.g., "button-primary" could be "button" or "button-primary")
77
+ const normalizedName = componentType.toLowerCase();
78
+
79
+ // Also try the base name (for variants like "button-primary" -> "button")
80
+ const baseName = normalizedName.split("-")[0];
81
+
82
+ // Strategy 1: Check common patterns with full name first
83
+ for (const pattern of SEARCH_PATTERNS) {
84
+ const searchDir = path.join(projectRoot, pattern);
85
+ if (fs.existsSync(searchDir)) {
86
+ // Try full name first
87
+ for (const ext of EXTENSIONS) {
88
+ const fullPath = path.join(searchDir, `${normalizedName}${ext}`);
89
+ if (fs.existsSync(fullPath)) {
90
+ return fullPath;
91
+ }
92
+ }
93
+ // Then try base name
94
+ if (baseName !== normalizedName) {
95
+ for (const ext of EXTENSIONS) {
96
+ const fullPath = path.join(searchDir, `${baseName}${ext}`);
97
+ if (fs.existsSync(fullPath)) {
98
+ return fullPath;
99
+ }
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ // Strategy 2: Recursive search from src/components or components
106
+ const recursiveDirs = ["src/components", "components", "src", "app"];
107
+ for (const dir of recursiveDirs) {
108
+ const searchDir = path.join(projectRoot, dir);
109
+ if (fs.existsSync(searchDir)) {
110
+ // Try full name first
111
+ const result = findFileRecursive(searchDir, normalizedName);
112
+ if (result) return result;
113
+
114
+ // Then try base name
115
+ if (baseName !== normalizedName) {
116
+ const baseResult = findFileRecursive(searchDir, baseName);
117
+ if (baseResult) return baseResult;
118
+ }
119
+ }
120
+ }
121
+
122
+ return null;
123
+ }
124
+
125
+ export async function GET(request: Request) {
126
+ // Security: Only allow in development
127
+ if (process.env.NODE_ENV !== "development") {
128
+ return NextResponse.json(
129
+ { error: "This endpoint is only available in development mode." },
130
+ { status: 403 }
131
+ );
132
+ }
133
+
134
+ try {
135
+ const { searchParams } = new URL(request.url);
136
+ const componentType = searchParams.get("component");
137
+
138
+ if (!componentType) {
139
+ return NextResponse.json(
140
+ { error: "component parameter is required" },
141
+ { status: 400 }
142
+ );
143
+ }
144
+
145
+ const projectRoot = process.cwd();
146
+ const foundPath = findComponentFile(projectRoot, componentType);
147
+
148
+ if (!foundPath) {
149
+ return NextResponse.json({
150
+ found: false,
151
+ componentType,
152
+ searchedPatterns: SEARCH_PATTERNS,
153
+ message: `Could not find component file for "${componentType}"`,
154
+ });
155
+ }
156
+
157
+ // Return relative path from project root
158
+ const relativePath = path.relative(projectRoot, foundPath);
159
+
160
+ return NextResponse.json({
161
+ found: true,
162
+ filePath: relativePath,
163
+ componentType,
164
+ absolutePath: foundPath,
165
+ });
166
+ } catch (error) {
167
+ console.error("Error finding component:", error);
168
+ return NextResponse.json(
169
+ { error: "Failed to find component", details: String(error) },
170
+ { status: 500 }
171
+ );
172
+ }
173
+ }
174
+
@@ -0,0 +1,181 @@
1
+ import { NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ /**
6
+ * Sonance DevTools API - Save Colors
7
+ *
8
+ * Persists color changes to the codebase by patching source files.
9
+ * Analyzes the codebase structure to determine HOW to save:
10
+ * - CSS Variables: Patch :root declarations
11
+ * - Theme files: Update JS/TS color objects
12
+ * - Hardcoded: Find and replace hex values
13
+ *
14
+ * DEVELOPMENT ONLY.
15
+ */
16
+
17
+ interface ColorPatchRequest {
18
+ primaryColor?: string;
19
+ accentColor?: string;
20
+ colorArchitecture: {
21
+ primary: "css-variables" | "tailwind" | "hardcoded" | "unknown";
22
+ accent: "css-variables" | "tailwind" | "hardcoded" | "unknown";
23
+ sources: {
24
+ filePath: string;
25
+ type: "css-variables" | "tailwind-config" | "theme-file" | "hardcoded";
26
+ variables: {
27
+ name: string;
28
+ value: string;
29
+ lineNumber: number;
30
+ }[];
31
+ }[];
32
+ };
33
+ }
34
+
35
+ interface PatchResult {
36
+ file: string;
37
+ success: boolean;
38
+ changes: number;
39
+ error?: string;
40
+ }
41
+
42
+ export async function POST(request: Request) {
43
+ // Security: Only allow in development
44
+ if (process.env.NODE_ENV !== "development") {
45
+ return NextResponse.json(
46
+ { error: "This endpoint is only available in development mode." },
47
+ { status: 403 }
48
+ );
49
+ }
50
+
51
+ try {
52
+ const body: ColorPatchRequest = await request.json();
53
+ const { primaryColor, accentColor, colorArchitecture } = body;
54
+
55
+ if (!primaryColor && !accentColor) {
56
+ return NextResponse.json(
57
+ { error: "No color changes provided" },
58
+ { status: 400 }
59
+ );
60
+ }
61
+
62
+ const projectRoot = process.cwd();
63
+ const results: PatchResult[] = [];
64
+
65
+ // Process each source file
66
+ for (const source of colorArchitecture.sources) {
67
+ const fullPath = path.join(projectRoot, source.filePath);
68
+
69
+ if (!fs.existsSync(fullPath)) {
70
+ results.push({
71
+ file: source.filePath,
72
+ success: false,
73
+ changes: 0,
74
+ error: "File not found",
75
+ });
76
+ continue;
77
+ }
78
+
79
+ try {
80
+ let content = fs.readFileSync(fullPath, "utf-8");
81
+ let changes = 0;
82
+
83
+ // Patch each variable in this file
84
+ for (const variable of source.variables) {
85
+ const isPrimaryVar = variable.name.toLowerCase().includes("primary") ||
86
+ variable.name === "baseColor";
87
+ const isAccentVar = variable.name.toLowerCase().includes("accent") ||
88
+ variable.name === "accentColor";
89
+
90
+ let newValue: string | null = null;
91
+ if (isPrimaryVar && primaryColor) {
92
+ newValue = primaryColor;
93
+ } else if (isAccentVar && accentColor) {
94
+ newValue = accentColor;
95
+ }
96
+
97
+ if (!newValue) continue;
98
+
99
+ // Different patching strategies based on source type
100
+ if (source.type === "css-variables") {
101
+ // Pattern: --variable-name: #oldvalue;
102
+ const cssPattern = new RegExp(
103
+ `(${escapeRegex(variable.name)}\\s*:\\s*)(${escapeRegex(variable.value)}|#[0-9a-fA-F]{3,8})`,
104
+ "g"
105
+ );
106
+ const newContent = content.replace(cssPattern, `$1${newValue}`);
107
+ if (newContent !== content) {
108
+ content = newContent;
109
+ changes++;
110
+ }
111
+ } else if (source.type === "theme-file" || source.type === "hardcoded") {
112
+ // Pattern: variableName: "#oldvalue" or variableName = "#oldvalue"
113
+ const jsPattern = new RegExp(
114
+ `(${escapeRegex(variable.name)}\\s*[:=]\\s*['"\`])(${escapeRegex(variable.value)}|#[0-9a-fA-F]{3,8})(['"\`])`,
115
+ "g"
116
+ );
117
+ const newContent = content.replace(jsPattern, `$1${newValue}$3`);
118
+ if (newContent !== content) {
119
+ content = newContent;
120
+ changes++;
121
+ }
122
+ } else if (source.type === "tailwind-config") {
123
+ // Pattern: primary: "#oldvalue" or primary: '#oldvalue'
124
+ const tailwindPattern = new RegExp(
125
+ `(${escapeRegex(variable.name)}\\s*:\\s*['"])(${escapeRegex(variable.value)}|#[0-9a-fA-F]{3,8})(['"])`,
126
+ "g"
127
+ );
128
+ const newContent = content.replace(tailwindPattern, `$1${newValue}$3`);
129
+ if (newContent !== content) {
130
+ content = newContent;
131
+ changes++;
132
+ }
133
+ }
134
+ }
135
+
136
+ if (changes > 0) {
137
+ fs.writeFileSync(fullPath, content, "utf-8");
138
+ results.push({
139
+ file: source.filePath,
140
+ success: true,
141
+ changes,
142
+ });
143
+ } else {
144
+ results.push({
145
+ file: source.filePath,
146
+ success: true,
147
+ changes: 0,
148
+ });
149
+ }
150
+ } catch (error) {
151
+ results.push({
152
+ file: source.filePath,
153
+ success: false,
154
+ changes: 0,
155
+ error: String(error),
156
+ });
157
+ }
158
+ }
159
+
160
+ const totalChanges = results.reduce((sum, r) => sum + r.changes, 0);
161
+ const successFiles = results.filter(r => r.success && r.changes > 0).length;
162
+
163
+ return NextResponse.json({
164
+ success: true,
165
+ message: `Updated ${totalChanges} color value(s) in ${successFiles} file(s).`,
166
+ results,
167
+ });
168
+ } catch (error) {
169
+ console.error("Error saving colors:", error);
170
+ return NextResponse.json(
171
+ { error: "Failed to save colors", details: String(error) },
172
+ { status: 500 }
173
+ );
174
+ }
175
+ }
176
+
177
+ // Helper to escape special regex characters
178
+ function escapeRegex(str: string): string {
179
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
+ }
181
+