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.
- package/dist/assets/api/sonance-ai-edit/route.ts +485 -0
- package/dist/assets/api/sonance-component-source/route.ts +78 -0
- package/dist/assets/api/sonance-find-component/route.ts +174 -0
- package/dist/assets/api/sonance-save-colors/route.ts +181 -0
- package/dist/assets/api/sonance-vision-apply/route.ts +652 -0
- package/dist/assets/api/sonance-vision-edit/route.ts +532 -0
- package/dist/index.js +67 -0
- package/package.json +1 -1
|
@@ -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
|
+
|