sonance-brand-mcp 1.3.18 → 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 +82 -0
- package/package.json +1 -1
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sonance DevTools API - Apply-First Vision Mode
|
|
9
|
+
*
|
|
10
|
+
* Cursor-style workflow: Apply changes immediately with backups, let user accept/revert.
|
|
11
|
+
*
|
|
12
|
+
* Actions:
|
|
13
|
+
* - "apply": Generate AI changes, create backups, write files immediately
|
|
14
|
+
* - "accept": Delete backups (user confirmed changes)
|
|
15
|
+
* - "revert": Restore files from backups
|
|
16
|
+
*
|
|
17
|
+
* DEVELOPMENT ONLY.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface VisionFocusedElement {
|
|
21
|
+
name: string;
|
|
22
|
+
type: string;
|
|
23
|
+
variantId?: string;
|
|
24
|
+
coordinates: {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
};
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface VisionFileModification {
|
|
34
|
+
filePath: string;
|
|
35
|
+
originalContent: string;
|
|
36
|
+
modifiedContent: string;
|
|
37
|
+
diff: string;
|
|
38
|
+
explanation: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ApplyFirstRequest {
|
|
42
|
+
action: "apply" | "accept" | "revert";
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
screenshot?: string;
|
|
45
|
+
pageRoute?: string;
|
|
46
|
+
userPrompt?: string;
|
|
47
|
+
focusedElements?: VisionFocusedElement[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface BackupManifest {
|
|
51
|
+
sessionId: string;
|
|
52
|
+
timestamp: number;
|
|
53
|
+
files: { original: string; backup: string }[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const BACKUP_ROOT = ".sonance-backups";
|
|
57
|
+
|
|
58
|
+
const VISION_SYSTEM_PROMPT = `You are a React/Tailwind CSS expert with vision capabilities for the Sonance brand system.
|
|
59
|
+
|
|
60
|
+
You can see screenshots of web pages and understand their visual layout, then modify code to implement requested changes.
|
|
61
|
+
|
|
62
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
63
|
+
CRITICAL RULES
|
|
64
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
**ANALYSIS:**
|
|
67
|
+
1. CAREFULLY analyze the screenshot to understand the current visual state
|
|
68
|
+
2. Identify elements mentioned in the user's request
|
|
69
|
+
3. Understand the current styling and layout
|
|
70
|
+
4. Consider how changes will affect the overall design
|
|
71
|
+
|
|
72
|
+
**PRESERVATION RULES:**
|
|
73
|
+
5. NEVER delete or remove existing content, children, or JSX elements
|
|
74
|
+
6. NEVER change component structure unless specifically requested
|
|
75
|
+
7. NEVER modify TypeScript types, imports, or exports unless necessary
|
|
76
|
+
8. NEVER remove data-sonance-* attributes
|
|
77
|
+
|
|
78
|
+
**CHANGE RULES:**
|
|
79
|
+
9. Make ONLY the changes requested by the user
|
|
80
|
+
10. Modify the minimum amount of code necessary
|
|
81
|
+
11. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
82
|
+
12. Maintain dark mode compatibility with CSS variables
|
|
83
|
+
13. Keep the cn() utility for className merging
|
|
84
|
+
|
|
85
|
+
**SONANCE BRAND COLORS:**
|
|
86
|
+
- Charcoal: #333F48, #343D46 (primary)
|
|
87
|
+
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
88
|
+
- IPORT Orange: #FC4C02
|
|
89
|
+
- IPORT Dark: #0F161D
|
|
90
|
+
- Blaze Blue: #00A3E1
|
|
91
|
+
- Blaze Red: #C02B0A
|
|
92
|
+
|
|
93
|
+
**RESPONSE FORMAT:**
|
|
94
|
+
Return ONLY a valid JSON object. Do not include any conversational text before or after the JSON.
|
|
95
|
+
The JSON must include:
|
|
96
|
+
- "reasoning": Brief explanation of what you see in the screenshot and your plan
|
|
97
|
+
- "modifications": Array of file modifications, each with:
|
|
98
|
+
- "filePath": Path to the file
|
|
99
|
+
- "modifiedContent": Complete updated file content
|
|
100
|
+
- "explanation": What changed in this file
|
|
101
|
+
- "explanation": Overall summary of changes`;
|
|
102
|
+
|
|
103
|
+
export async function POST(request: Request) {
|
|
104
|
+
// Only allow in development
|
|
105
|
+
if (process.env.NODE_ENV !== "development") {
|
|
106
|
+
return NextResponse.json(
|
|
107
|
+
{ error: "Apply-first mode is only available in development" },
|
|
108
|
+
{ status: 403 }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const body: ApplyFirstRequest = await request.json();
|
|
114
|
+
const { action, sessionId, screenshot, pageRoute, userPrompt, focusedElements } = body;
|
|
115
|
+
const projectRoot = process.cwd();
|
|
116
|
+
|
|
117
|
+
// ========== ACCEPT ACTION ==========
|
|
118
|
+
if (action === "accept") {
|
|
119
|
+
if (!sessionId) {
|
|
120
|
+
return NextResponse.json(
|
|
121
|
+
{ error: "sessionId is required for accept action" },
|
|
122
|
+
{ status: 400 }
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
|
|
127
|
+
|
|
128
|
+
// Just delete the backup directory
|
|
129
|
+
if (fs.existsSync(backupDir)) {
|
|
130
|
+
fs.rmSync(backupDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return NextResponse.json({
|
|
134
|
+
success: true,
|
|
135
|
+
message: "Changes accepted, backups deleted",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ========== REVERT ACTION ==========
|
|
140
|
+
if (action === "revert") {
|
|
141
|
+
if (!sessionId) {
|
|
142
|
+
return NextResponse.json(
|
|
143
|
+
{ error: "sessionId is required for revert action" },
|
|
144
|
+
{ status: 400 }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const result = await revertFromBackups(sessionId, projectRoot);
|
|
149
|
+
|
|
150
|
+
return NextResponse.json({
|
|
151
|
+
success: result.success,
|
|
152
|
+
message: result.message,
|
|
153
|
+
filesReverted: result.filesReverted,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ========== APPLY ACTION ==========
|
|
158
|
+
if (action === "apply") {
|
|
159
|
+
if (!userPrompt) {
|
|
160
|
+
return NextResponse.json(
|
|
161
|
+
{ error: "userPrompt is required for apply action" },
|
|
162
|
+
{ status: 400 }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check for API key
|
|
167
|
+
const apiKey = process.env.NEXT_PUBLIC_CLAUDE_API_KEY;
|
|
168
|
+
if (!apiKey) {
|
|
169
|
+
return NextResponse.json(
|
|
170
|
+
{ error: "NEXT_PUBLIC_CLAUDE_API_KEY not configured. Add it to your .env.local file." },
|
|
171
|
+
{ status: 500 }
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Generate a unique session ID
|
|
176
|
+
const newSessionId = randomUUID().slice(0, 8);
|
|
177
|
+
|
|
178
|
+
// Gather page context
|
|
179
|
+
const pageContext = gatherPageContext(pageRoute || "/", projectRoot);
|
|
180
|
+
|
|
181
|
+
// Build user message with vision
|
|
182
|
+
const messageContent: Anthropic.MessageCreateParams["messages"][0]["content"] = [];
|
|
183
|
+
|
|
184
|
+
// Add screenshot if provided
|
|
185
|
+
if (screenshot) {
|
|
186
|
+
const base64Data = screenshot.split(",")[1] || screenshot;
|
|
187
|
+
messageContent.push({
|
|
188
|
+
type: "image",
|
|
189
|
+
source: {
|
|
190
|
+
type: "base64",
|
|
191
|
+
media_type: "image/png",
|
|
192
|
+
data: base64Data,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Build text content
|
|
198
|
+
let textContent = `VISION MODE EDIT REQUEST
|
|
199
|
+
|
|
200
|
+
Page Route: ${pageRoute}
|
|
201
|
+
User Request: "${userPrompt}"
|
|
202
|
+
|
|
203
|
+
`;
|
|
204
|
+
|
|
205
|
+
if (focusedElements && focusedElements.length > 0) {
|
|
206
|
+
textContent += `FOCUSED ELEMENTS (user clicked on these):
|
|
207
|
+
${focusedElements.map((el) => `- ${el.name} (${el.type}) at (${el.coordinates.x}, ${el.coordinates.y})`).join("\n")}
|
|
208
|
+
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
textContent += `PAGE CONTEXT:
|
|
213
|
+
|
|
214
|
+
Page File: ${pageContext.pageFile || "Not found"}
|
|
215
|
+
${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""}
|
|
216
|
+
|
|
217
|
+
`;
|
|
218
|
+
|
|
219
|
+
if (pageContext.componentSources.length > 0) {
|
|
220
|
+
textContent += `IMPORTED COMPONENTS:\n`;
|
|
221
|
+
for (const comp of pageContext.componentSources) {
|
|
222
|
+
textContent += `
|
|
223
|
+
File: ${comp.path}
|
|
224
|
+
\`\`\`tsx
|
|
225
|
+
${comp.content.substring(0, 3000)}${comp.content.length > 3000 ? "\n// ... (truncated)" : ""}
|
|
226
|
+
\`\`\`
|
|
227
|
+
`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
textContent += `
|
|
232
|
+
GLOBALS.CSS (relevant theme variables):
|
|
233
|
+
\`\`\`css
|
|
234
|
+
${pageContext.globalsCSS.substring(0, 2000)}${pageContext.globalsCSS.length > 2000 ? "\n/* ... (truncated) */" : ""}
|
|
235
|
+
\`\`\`
|
|
236
|
+
|
|
237
|
+
INSTRUCTIONS:
|
|
238
|
+
1. Look at the screenshot and identify elements mentioned in the user's request
|
|
239
|
+
2. Review the code to understand current implementation
|
|
240
|
+
3. Determine which files need modifications
|
|
241
|
+
4. Generate complete modified code for each file
|
|
242
|
+
5. Return as JSON in the specified format`;
|
|
243
|
+
|
|
244
|
+
messageContent.push({
|
|
245
|
+
type: "text",
|
|
246
|
+
text: textContent,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Call Claude Vision API
|
|
250
|
+
const anthropic = new Anthropic({ apiKey });
|
|
251
|
+
|
|
252
|
+
const response = await anthropic.messages.create({
|
|
253
|
+
model: "claude-sonnet-4-20250514",
|
|
254
|
+
max_tokens: 16384,
|
|
255
|
+
messages: [
|
|
256
|
+
{
|
|
257
|
+
role: "user",
|
|
258
|
+
content: messageContent,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
system: VISION_SYSTEM_PROMPT,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Extract text content from response
|
|
265
|
+
const textResponse = response.content.find((block) => block.type === "text");
|
|
266
|
+
if (!textResponse || textResponse.type !== "text") {
|
|
267
|
+
return NextResponse.json(
|
|
268
|
+
{ error: "No text response from AI" },
|
|
269
|
+
{ status: 500 }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Parse AI response
|
|
274
|
+
let aiResponse: {
|
|
275
|
+
reasoning?: string;
|
|
276
|
+
modifications: Array<{
|
|
277
|
+
filePath: string;
|
|
278
|
+
modifiedContent: string;
|
|
279
|
+
explanation: string;
|
|
280
|
+
}>;
|
|
281
|
+
explanation?: string;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
let jsonText = textResponse.text.trim();
|
|
286
|
+
|
|
287
|
+
// Try to extract JSON from markdown code blocks
|
|
288
|
+
const jsonMatch = jsonText.match(/```json\n([\s\S]*?)\n```/) ||
|
|
289
|
+
jsonText.match(/```\n([\s\S]*?)\n```/);
|
|
290
|
+
|
|
291
|
+
if (jsonMatch) {
|
|
292
|
+
jsonText = jsonMatch[1];
|
|
293
|
+
} else if (jsonText.includes("```json")) {
|
|
294
|
+
const start = jsonText.indexOf("```json") + 7;
|
|
295
|
+
const end = jsonText.lastIndexOf("```");
|
|
296
|
+
if (end > start) {
|
|
297
|
+
jsonText = jsonText.substring(start, end);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
jsonText = jsonText.trim();
|
|
302
|
+
aiResponse = JSON.parse(jsonText);
|
|
303
|
+
} catch {
|
|
304
|
+
console.error("Failed to parse AI response:", textResponse.text);
|
|
305
|
+
return NextResponse.json(
|
|
306
|
+
{ error: "Failed to parse AI response. Please try again." },
|
|
307
|
+
{ status: 500 }
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!aiResponse.modifications || aiResponse.modifications.length === 0) {
|
|
312
|
+
return NextResponse.json({
|
|
313
|
+
success: true,
|
|
314
|
+
sessionId: newSessionId,
|
|
315
|
+
modifications: [],
|
|
316
|
+
explanation: aiResponse.explanation || "No changes needed.",
|
|
317
|
+
reasoning: aiResponse.reasoning,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Read original content and prepare modifications
|
|
322
|
+
const modifications: VisionFileModification[] = [];
|
|
323
|
+
for (const mod of aiResponse.modifications) {
|
|
324
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
325
|
+
let originalContent = "";
|
|
326
|
+
if (fs.existsSync(fullPath)) {
|
|
327
|
+
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
modifications.push({
|
|
331
|
+
filePath: mod.filePath,
|
|
332
|
+
originalContent,
|
|
333
|
+
modifiedContent: mod.modifiedContent,
|
|
334
|
+
diff: generateSimpleDiff(originalContent, mod.modifiedContent),
|
|
335
|
+
explanation: mod.explanation,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Create backups and apply changes atomically
|
|
340
|
+
const applyResult = await applyChangesWithBackup(
|
|
341
|
+
modifications,
|
|
342
|
+
newSessionId,
|
|
343
|
+
projectRoot
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (!applyResult.success) {
|
|
347
|
+
return NextResponse.json(
|
|
348
|
+
{ error: applyResult.error || "Failed to apply changes" },
|
|
349
|
+
{ status: 500 }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return NextResponse.json({
|
|
354
|
+
success: true,
|
|
355
|
+
sessionId: newSessionId,
|
|
356
|
+
modifications,
|
|
357
|
+
backupPaths: applyResult.backupPaths,
|
|
358
|
+
explanation: aiResponse.explanation,
|
|
359
|
+
reasoning: aiResponse.reasoning,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return NextResponse.json(
|
|
364
|
+
{ error: "Invalid action. Use 'apply', 'accept', or 'revert'." },
|
|
365
|
+
{ status: 400 }
|
|
366
|
+
);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error("Apply-first error:", error);
|
|
369
|
+
return NextResponse.json(
|
|
370
|
+
{ error: "Failed to process request", details: String(error) },
|
|
371
|
+
{ status: 500 }
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Apply changes with backup - atomic operation
|
|
378
|
+
*/
|
|
379
|
+
async function applyChangesWithBackup(
|
|
380
|
+
modifications: VisionFileModification[],
|
|
381
|
+
sessionId: string,
|
|
382
|
+
projectRoot: string
|
|
383
|
+
): Promise<{ success: boolean; backupPaths: string[]; error?: string }> {
|
|
384
|
+
const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
|
|
385
|
+
const backupPaths: string[] = [];
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
// Step 1: Create backup directory
|
|
389
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
390
|
+
|
|
391
|
+
// Step 2: Create ALL backups FIRST (before any writes)
|
|
392
|
+
for (const mod of modifications) {
|
|
393
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
394
|
+
|
|
395
|
+
if (fs.existsSync(fullPath)) {
|
|
396
|
+
const backupPath = path.join(backupDir, mod.filePath);
|
|
397
|
+
const backupDirForFile = path.dirname(backupPath);
|
|
398
|
+
|
|
399
|
+
fs.mkdirSync(backupDirForFile, { recursive: true });
|
|
400
|
+
fs.copyFileSync(fullPath, backupPath);
|
|
401
|
+
backupPaths.push(backupPath);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Step 3: Write manifest
|
|
406
|
+
const manifest: BackupManifest = {
|
|
407
|
+
sessionId,
|
|
408
|
+
timestamp: Date.now(),
|
|
409
|
+
files: modifications.map(m => ({
|
|
410
|
+
original: m.filePath,
|
|
411
|
+
backup: path.join(backupDir, m.filePath),
|
|
412
|
+
})),
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
fs.writeFileSync(
|
|
416
|
+
path.join(backupDir, "manifest.json"),
|
|
417
|
+
JSON.stringify(manifest, null, 2)
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// Step 4: Apply all changes
|
|
421
|
+
for (const mod of modifications) {
|
|
422
|
+
const fullPath = path.join(projectRoot, mod.filePath);
|
|
423
|
+
|
|
424
|
+
// Validate path is within project
|
|
425
|
+
if (!fullPath.startsWith(projectRoot)) {
|
|
426
|
+
throw new Error(`Invalid file path: ${mod.filePath}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Ensure directory exists
|
|
430
|
+
const dir = path.dirname(fullPath);
|
|
431
|
+
if (!fs.existsSync(dir)) {
|
|
432
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fs.writeFileSync(fullPath, mod.modifiedContent, "utf-8");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return { success: true, backupPaths };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
// Rollback on error - restore all backed up files
|
|
441
|
+
console.error("Error applying changes, rolling back:", error);
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
await revertFromBackups(sessionId, projectRoot);
|
|
445
|
+
} catch (rollbackError) {
|
|
446
|
+
console.error("Rollback also failed:", rollbackError);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
backupPaths: [],
|
|
452
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Revert files from backups
|
|
459
|
+
*/
|
|
460
|
+
async function revertFromBackups(
|
|
461
|
+
sessionId: string,
|
|
462
|
+
projectRoot: string
|
|
463
|
+
): Promise<{ success: boolean; message: string; filesReverted: number }> {
|
|
464
|
+
const backupDir = path.join(projectRoot, BACKUP_ROOT, sessionId);
|
|
465
|
+
|
|
466
|
+
if (!fs.existsSync(backupDir)) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
message: "Backup session not found",
|
|
470
|
+
filesReverted: 0,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const manifestPath = path.join(backupDir, "manifest.json");
|
|
476
|
+
|
|
477
|
+
if (!fs.existsSync(manifestPath)) {
|
|
478
|
+
// If no manifest, try to restore any backup files we find
|
|
479
|
+
return {
|
|
480
|
+
success: false,
|
|
481
|
+
message: "Backup manifest not found",
|
|
482
|
+
filesReverted: 0,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const manifest: BackupManifest = JSON.parse(
|
|
487
|
+
fs.readFileSync(manifestPath, "utf-8")
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
let filesReverted = 0;
|
|
491
|
+
|
|
492
|
+
for (const file of manifest.files) {
|
|
493
|
+
const backupPath = path.join(backupDir, file.original);
|
|
494
|
+
const originalPath = path.join(projectRoot, file.original);
|
|
495
|
+
|
|
496
|
+
if (fs.existsSync(backupPath)) {
|
|
497
|
+
fs.copyFileSync(backupPath, originalPath);
|
|
498
|
+
filesReverted++;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Delete backup directory after successful revert
|
|
503
|
+
fs.rmSync(backupDir, { recursive: true });
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
message: `Reverted ${filesReverted} file(s)`,
|
|
508
|
+
filesReverted,
|
|
509
|
+
};
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error("Error reverting from backups:", error);
|
|
512
|
+
return {
|
|
513
|
+
success: false,
|
|
514
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
515
|
+
filesReverted: 0,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Gather context about the current page for AI analysis
|
|
522
|
+
*/
|
|
523
|
+
function gatherPageContext(
|
|
524
|
+
pageRoute: string,
|
|
525
|
+
projectRoot: string
|
|
526
|
+
): {
|
|
527
|
+
pageFile: string | null;
|
|
528
|
+
pageContent: string;
|
|
529
|
+
componentSources: { path: string; content: string }[];
|
|
530
|
+
globalsCSS: string;
|
|
531
|
+
} {
|
|
532
|
+
const pageFile = discoverPageFile(pageRoute, projectRoot);
|
|
533
|
+
let pageContent = "";
|
|
534
|
+
const componentSources: { path: string; content: string }[] = [];
|
|
535
|
+
|
|
536
|
+
if (pageFile) {
|
|
537
|
+
const fullPath = path.join(projectRoot, pageFile);
|
|
538
|
+
if (fs.existsSync(fullPath)) {
|
|
539
|
+
pageContent = fs.readFileSync(fullPath, "utf-8");
|
|
540
|
+
|
|
541
|
+
const imports = extractImports(pageContent);
|
|
542
|
+
for (const importPath of imports) {
|
|
543
|
+
const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
|
|
544
|
+
if (resolvedPath && fs.existsSync(path.join(projectRoot, resolvedPath))) {
|
|
545
|
+
try {
|
|
546
|
+
const content = fs.readFileSync(path.join(projectRoot, resolvedPath), "utf-8");
|
|
547
|
+
componentSources.push({ path: resolvedPath, content });
|
|
548
|
+
} catch {
|
|
549
|
+
// Skip files that can't be read
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
let globalsCSS = "";
|
|
557
|
+
const globalsPath = path.join(projectRoot, "src/app/globals.css");
|
|
558
|
+
if (fs.existsSync(globalsPath)) {
|
|
559
|
+
globalsCSS = fs.readFileSync(globalsPath, "utf-8");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return { pageFile, pageContent, componentSources, globalsCSS };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
566
|
+
if (route === "/" || route === "") {
|
|
567
|
+
const rootPage = "src/app/page.tsx";
|
|
568
|
+
if (fs.existsSync(path.join(projectRoot, rootPage))) {
|
|
569
|
+
return rootPage;
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const cleanRoute = route.replace(/^\//, "");
|
|
575
|
+
const patterns = [
|
|
576
|
+
`src/app/${cleanRoute}/page.tsx`,
|
|
577
|
+
`src/app/${cleanRoute}/page.jsx`,
|
|
578
|
+
`src/app/${cleanRoute}.tsx`,
|
|
579
|
+
];
|
|
580
|
+
|
|
581
|
+
for (const pattern of patterns) {
|
|
582
|
+
if (fs.existsSync(path.join(projectRoot, pattern))) {
|
|
583
|
+
return pattern;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function extractImports(content: string): string[] {
|
|
591
|
+
const importRegex = /import\s+.*?\s+from\s+["'](@\/components\/[^"']+|\.\.?\/[^"']+)["']/g;
|
|
592
|
+
const imports: string[] = [];
|
|
593
|
+
let match;
|
|
594
|
+
|
|
595
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
596
|
+
imports.push(match[1]);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return imports;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function resolveImportPath(
|
|
603
|
+
importPath: string,
|
|
604
|
+
fromFile: string,
|
|
605
|
+
projectRoot: string
|
|
606
|
+
): string | null {
|
|
607
|
+
if (importPath.startsWith("@/")) {
|
|
608
|
+
const resolved = importPath.replace("@/", "src/");
|
|
609
|
+
const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
|
|
610
|
+
if (fs.existsSync(path.join(projectRoot, withExt))) {
|
|
611
|
+
return withExt;
|
|
612
|
+
}
|
|
613
|
+
const indexPath = `${resolved}/index.tsx`;
|
|
614
|
+
if (fs.existsSync(path.join(projectRoot, indexPath))) {
|
|
615
|
+
return indexPath;
|
|
616
|
+
}
|
|
617
|
+
return withExt;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (importPath.startsWith(".")) {
|
|
621
|
+
const dir = path.dirname(fromFile);
|
|
622
|
+
const resolved = path.join(dir, importPath);
|
|
623
|
+
const withExt = resolved.endsWith(".tsx") ? resolved : `${resolved}.tsx`;
|
|
624
|
+
return withExt;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function generateSimpleDiff(original: string, modified: string): string {
|
|
631
|
+
const originalLines = original.split("\n");
|
|
632
|
+
const modifiedLines = modified.split("\n");
|
|
633
|
+
|
|
634
|
+
const diff: string[] = [];
|
|
635
|
+
const maxLines = Math.max(originalLines.length, modifiedLines.length);
|
|
636
|
+
|
|
637
|
+
for (let i = 0; i < maxLines; i++) {
|
|
638
|
+
const origLine = originalLines[i];
|
|
639
|
+
const modLine = modifiedLines[i];
|
|
640
|
+
|
|
641
|
+
if (origLine === undefined && modLine !== undefined) {
|
|
642
|
+
diff.push(`+ ${modLine}`);
|
|
643
|
+
} else if (origLine !== undefined && modLine === undefined) {
|
|
644
|
+
diff.push(`- ${origLine}`);
|
|
645
|
+
} else if (origLine !== modLine) {
|
|
646
|
+
diff.push(`- ${origLine}`);
|
|
647
|
+
diff.push(`+ ${modLine}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return diff.join("\n");
|
|
652
|
+
}
|