sonance-brand-mcp 1.3.24 → 1.3.26
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.
|
@@ -55,69 +55,63 @@ interface BackupManifest {
|
|
|
55
55
|
|
|
56
56
|
const BACKUP_ROOT = ".sonance-backups";
|
|
57
57
|
|
|
58
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
58
|
+
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
61
|
+
UNDERSTAND THE USER'S REQUEST NATURALLY
|
|
62
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
63
|
+
|
|
64
|
+
Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
|
|
65
|
+
|
|
66
|
+
- If they say "add X", ADD X to the code (don't replace or remove anything else)
|
|
67
|
+
- If they say "change X to Y", find X and change it to Y
|
|
68
|
+
- If they say "make X bigger/smaller/different color", adjust the relevant properties
|
|
69
|
+
- If they say "remove X", remove X
|
|
70
|
+
- If they say "wrap X with Y", add Y as a parent around X
|
|
71
|
+
|
|
72
|
+
For any change:
|
|
73
|
+
1. Read the code context provided
|
|
74
|
+
2. Understand what the user wants
|
|
75
|
+
3. Generate patches that accomplish exactly that
|
|
76
|
+
4. If adding a component/icon requires an import, include a patch for the import too
|
|
77
|
+
|
|
78
|
+
Don't overthink - just make the change the user requested.
|
|
61
79
|
|
|
62
80
|
═══════════════════════════════════════════════════════════════════════════════
|
|
63
|
-
|
|
81
|
+
PATCH FORMAT
|
|
64
82
|
═══════════════════════════════════════════════════════════════════════════════
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
**FILE RULES:**
|
|
74
|
-
6. You may ONLY edit files that are provided in the PAGE CONTEXT section
|
|
75
|
-
7. NEVER create new files - only modify existing ones shown to you
|
|
76
|
-
8. The filePath in your response MUST exactly match one of the provided file paths
|
|
77
|
-
|
|
78
|
-
**PRESERVATION RULES (ZERO TOLERANCE):**
|
|
79
|
-
9. NEVER delete or remove existing functions, hooks, state, or logic
|
|
80
|
-
10. NEVER remove imports - you may only ADD imports if needed
|
|
81
|
-
11. NEVER remove useEffect, useState, useCallback, or other React hooks
|
|
82
|
-
12. NEVER remove API calls, fetch requests, or data loading logic
|
|
83
|
-
13. NEVER remove error handling, loading states, or conditional rendering
|
|
84
|
-
14. NEVER remove data-sonance-* attributes
|
|
85
|
-
15. NEVER change component structure unless specifically requested
|
|
86
|
-
16. NEVER modify TypeScript types or interfaces unless specifically requested
|
|
87
|
-
|
|
88
|
-
**CHANGE RULES:**
|
|
89
|
-
17. Make ONLY the changes requested by the user
|
|
90
|
-
18. Modify the MINIMUM amount of code necessary
|
|
91
|
-
19. Keep all existing className values and ADD to them if needed
|
|
92
|
-
20. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
93
|
-
21. Maintain dark mode compatibility with CSS variables
|
|
94
|
-
22. Keep the cn() utility for className merging
|
|
95
|
-
23. CRITICAL: You MUST return the FULL file content in "modifiedContent". Do NOT use comments like "// ... existing code ..." or "// ... rest of file ...". Return every single line of code, even if unchanged.
|
|
84
|
+
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
85
|
+
|
|
86
|
+
**Patch Rules:**
|
|
87
|
+
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
88
|
+
- "replace" contains your modified version
|
|
89
|
+
- Include 2-4 lines of context in "search" to make it unique
|
|
90
|
+
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
96
91
|
|
|
97
92
|
**SONANCE BRAND COLORS:**
|
|
98
|
-
- Charcoal: #333F48
|
|
99
|
-
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
93
|
+
- Charcoal: #333F48 (primary text)
|
|
100
94
|
- IPORT Orange: #FC4C02
|
|
101
|
-
- IPORT Dark: #0F161D
|
|
102
95
|
- Blaze Blue: #00A3E1
|
|
103
|
-
- Blaze Red: #C02B0A
|
|
104
96
|
|
|
105
97
|
**RESPONSE FORMAT:**
|
|
106
|
-
Return ONLY
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
98
|
+
Return ONLY valid JSON:
|
|
99
|
+
{
|
|
100
|
+
"reasoning": "What you understood from the request and your plan",
|
|
101
|
+
"modifications": [
|
|
102
|
+
{
|
|
103
|
+
"filePath": "path/to/file.tsx",
|
|
104
|
+
"patches": [
|
|
105
|
+
{
|
|
106
|
+
"search": "exact code to find",
|
|
107
|
+
"replace": "the replacement code",
|
|
108
|
+
"explanation": "what this patch does"
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"explanation": "summary of changes made"
|
|
114
|
+
}`;
|
|
121
115
|
|
|
122
116
|
export async function POST(request: Request) {
|
|
123
117
|
// Only allow in development
|
|
@@ -236,14 +230,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
|
|
|
236
230
|
`;
|
|
237
231
|
|
|
238
232
|
if (pageContext.componentSources.length > 0) {
|
|
239
|
-
|
|
240
|
-
|
|
233
|
+
// Smart truncation: prioritize first components (direct imports) and limit total context
|
|
234
|
+
const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
|
|
235
|
+
const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
|
|
236
|
+
const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
|
|
237
|
+
const MAX_FILES = 30; // Limit total number of files
|
|
238
|
+
|
|
239
|
+
let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
|
|
240
|
+
const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
|
|
241
|
+
|
|
242
|
+
textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < truncatedComponents.length; i++) {
|
|
245
|
+
const comp = truncatedComponents[i];
|
|
246
|
+
const isPriority = i < 10; // First 10 files are priority (direct imports)
|
|
247
|
+
const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
|
|
248
|
+
|
|
249
|
+
// Stop if we've used too much context
|
|
250
|
+
if (usedContext > MAX_TOTAL_CONTEXT) {
|
|
251
|
+
textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const truncatedContent = comp.content.substring(0, maxSize);
|
|
256
|
+
const wasTruncated = comp.content.length > maxSize;
|
|
257
|
+
|
|
241
258
|
textContent += `
|
|
242
|
-
File: ${comp.path}
|
|
259
|
+
File: ${comp.path}${isPriority ? '' : ' (nested)'}
|
|
243
260
|
\`\`\`tsx
|
|
244
|
-
${
|
|
261
|
+
${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
245
262
|
\`\`\`
|
|
246
263
|
`;
|
|
264
|
+
usedContext += truncatedContent.length;
|
|
247
265
|
}
|
|
248
266
|
}
|
|
249
267
|
|
|
@@ -296,13 +314,15 @@ CRITICAL: Your modified file should have approximately the same number of lines
|
|
|
296
314
|
);
|
|
297
315
|
}
|
|
298
316
|
|
|
299
|
-
// Parse AI response
|
|
317
|
+
// Parse AI response - now expecting patches instead of full file content
|
|
300
318
|
let aiResponse: {
|
|
301
319
|
reasoning?: string;
|
|
302
320
|
modifications: Array<{
|
|
303
321
|
filePath: string;
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
patches?: Patch[];
|
|
323
|
+
// Legacy support for modifiedContent (will be deprecated)
|
|
324
|
+
modifiedContent?: string;
|
|
325
|
+
explanation?: string;
|
|
306
326
|
}>;
|
|
307
327
|
explanation?: string;
|
|
308
328
|
};
|
|
@@ -344,54 +364,104 @@ CRITICAL: Your modified file should have approximately the same number of lines
|
|
|
344
364
|
});
|
|
345
365
|
}
|
|
346
366
|
|
|
347
|
-
//
|
|
367
|
+
// Build set of valid file paths from page context
|
|
368
|
+
const validFilePaths = new Set<string>();
|
|
369
|
+
if (pageContext.pageFile) {
|
|
370
|
+
validFilePaths.add(pageContext.pageFile);
|
|
371
|
+
}
|
|
372
|
+
for (const comp of pageContext.componentSources) {
|
|
373
|
+
validFilePaths.add(comp.path);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Process modifications - apply patches to get modified content
|
|
348
377
|
const modifications: VisionFileModification[] = [];
|
|
349
|
-
const
|
|
350
|
-
const validationWarnings: string[] = [];
|
|
378
|
+
const patchErrors: string[] = [];
|
|
351
379
|
|
|
352
380
|
for (const mod of aiResponse.modifications) {
|
|
381
|
+
// Validate that the file path is in the page context
|
|
382
|
+
// This prevents the AI from creating new files
|
|
383
|
+
if (!validFilePaths.has(mod.filePath)) {
|
|
384
|
+
console.warn(`[Apply-First] Rejected modification to unknown file: ${mod.filePath}`);
|
|
385
|
+
console.warn(`[Apply-First] Valid files are: ${Array.from(validFilePaths).join(", ")}`);
|
|
386
|
+
patchErrors.push(`${mod.filePath}: This file was not found in the page context. The AI can only modify existing files that are part of the current page.`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
353
390
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
354
391
|
let originalContent = "";
|
|
355
392
|
if (fs.existsSync(fullPath)) {
|
|
356
393
|
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
357
394
|
}
|
|
358
395
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
396
|
+
let modifiedContent: string;
|
|
397
|
+
let explanation = mod.explanation || "";
|
|
398
|
+
|
|
399
|
+
// Check if AI returned patches (new format) or modifiedContent (legacy)
|
|
400
|
+
if (mod.patches && mod.patches.length > 0) {
|
|
401
|
+
// New patch-based approach
|
|
402
|
+
console.log(`[Apply-First] Applying ${mod.patches.length} patches to ${mod.filePath}`);
|
|
403
|
+
|
|
404
|
+
const patchResult = applyPatches(originalContent, mod.patches);
|
|
405
|
+
|
|
406
|
+
if (!patchResult.success) {
|
|
407
|
+
const failedMessages = patchResult.failedPatches.map(
|
|
408
|
+
(f) => `Patch failed: ${f.error}`
|
|
409
|
+
).join("\n");
|
|
410
|
+
patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
|
|
411
|
+
|
|
412
|
+
// If some patches succeeded, use partial result
|
|
413
|
+
if (patchResult.appliedPatches > 0) {
|
|
414
|
+
console.warn(`[Apply-First] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
|
|
415
|
+
modifiedContent = patchResult.modifiedContent;
|
|
416
|
+
explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
|
|
417
|
+
} else {
|
|
418
|
+
continue; // Skip this file entirely if no patches worked
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
modifiedContent = patchResult.modifiedContent;
|
|
422
|
+
console.log(`[Apply-First] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
423
|
+
}
|
|
424
|
+
} else if (mod.modifiedContent) {
|
|
425
|
+
// Legacy: AI returned full file content
|
|
426
|
+
console.warn(`[Apply-First] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
427
|
+
modifiedContent = mod.modifiedContent;
|
|
428
|
+
|
|
429
|
+
// Validate the modification using legacy validation
|
|
430
|
+
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
431
|
+
if (!validation.valid) {
|
|
432
|
+
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
} else {
|
|
436
|
+
// No patches and no modifiedContent - skip
|
|
437
|
+
console.warn(`[Apply-First] No patches or modifiedContent for ${mod.filePath}`);
|
|
438
|
+
continue;
|
|
369
439
|
}
|
|
370
440
|
|
|
371
441
|
modifications.push({
|
|
372
442
|
filePath: mod.filePath,
|
|
373
443
|
originalContent,
|
|
374
|
-
modifiedContent
|
|
375
|
-
diff: generateSimpleDiff(originalContent,
|
|
376
|
-
explanation
|
|
444
|
+
modifiedContent,
|
|
445
|
+
diff: generateSimpleDiff(originalContent, modifiedContent),
|
|
446
|
+
explanation,
|
|
377
447
|
});
|
|
378
448
|
}
|
|
379
449
|
|
|
380
|
-
// If all modifications failed
|
|
381
|
-
if (
|
|
382
|
-
console.error("All AI
|
|
450
|
+
// If all modifications failed, return error
|
|
451
|
+
if (patchErrors.length > 0 && modifications.length === 0) {
|
|
452
|
+
console.error("All AI patches failed:", patchErrors);
|
|
383
453
|
return NextResponse.json(
|
|
384
454
|
{
|
|
385
455
|
success: false,
|
|
386
|
-
error:
|
|
456
|
+
error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
|
|
387
457
|
},
|
|
388
458
|
{ status: 400 }
|
|
389
459
|
);
|
|
390
460
|
}
|
|
391
461
|
|
|
392
|
-
// Log warnings
|
|
393
|
-
if (
|
|
394
|
-
console.warn("
|
|
462
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
463
|
+
if (patchErrors.length > 0) {
|
|
464
|
+
console.warn("Some patches failed:", patchErrors);
|
|
395
465
|
}
|
|
396
466
|
|
|
397
467
|
// Create backups and apply changes atomically
|
|
@@ -575,8 +645,109 @@ async function revertFromBackups(
|
|
|
575
645
|
}
|
|
576
646
|
}
|
|
577
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Recursively gather all imports from a file up to a max depth
|
|
650
|
+
* This builds a complete component graph for the AI to understand
|
|
651
|
+
*/
|
|
652
|
+
function gatherAllImports(
|
|
653
|
+
filePath: string,
|
|
654
|
+
projectRoot: string,
|
|
655
|
+
visited: Set<string> = new Set(),
|
|
656
|
+
maxDepth: number = 4
|
|
657
|
+
): { path: string; content: string }[] {
|
|
658
|
+
// Prevent infinite loops and limit total files
|
|
659
|
+
if (visited.has(filePath) || visited.size > 50) return [];
|
|
660
|
+
visited.add(filePath);
|
|
661
|
+
|
|
662
|
+
const results: { path: string; content: string }[] = [];
|
|
663
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
664
|
+
|
|
665
|
+
if (!fs.existsSync(fullPath)) return results;
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
669
|
+
results.push({ path: filePath, content });
|
|
670
|
+
|
|
671
|
+
// Continue recursing if we haven't hit max depth
|
|
672
|
+
if (maxDepth > 0) {
|
|
673
|
+
const imports = extractImports(content);
|
|
674
|
+
for (const imp of imports) {
|
|
675
|
+
const resolved = resolveImportPath(imp, filePath, projectRoot);
|
|
676
|
+
if (resolved && !visited.has(resolved)) {
|
|
677
|
+
const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
|
|
678
|
+
results.push(...nestedImports);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// Skip files that can't be read
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return results;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Discover layout files that wrap the page
|
|
691
|
+
* App Router: layout.tsx in same and parent directories
|
|
692
|
+
* Pages Router: _app.tsx and _document.tsx
|
|
693
|
+
*/
|
|
694
|
+
function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
|
|
695
|
+
const layoutFiles: string[] = [];
|
|
696
|
+
|
|
697
|
+
if (!pageFile) return layoutFiles;
|
|
698
|
+
|
|
699
|
+
// Determine if App Router or Pages Router
|
|
700
|
+
const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
|
|
701
|
+
const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
|
|
702
|
+
|
|
703
|
+
if (isAppRouter) {
|
|
704
|
+
// App Router: Check for layout.tsx in same directory and parent directories
|
|
705
|
+
let currentDir = path.dirname(pageFile);
|
|
706
|
+
const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
|
|
707
|
+
|
|
708
|
+
while (currentDir.includes(appRoot)) {
|
|
709
|
+
const layoutPatterns = [
|
|
710
|
+
path.join(currentDir, "layout.tsx"),
|
|
711
|
+
path.join(currentDir, "layout.jsx"),
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
for (const layoutPath of layoutPatterns) {
|
|
715
|
+
if (fs.existsSync(path.join(projectRoot, layoutPath))) {
|
|
716
|
+
layoutFiles.push(layoutPath);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Move to parent directory
|
|
721
|
+
const parentDir = path.dirname(currentDir);
|
|
722
|
+
if (parentDir === currentDir) break;
|
|
723
|
+
currentDir = parentDir;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (isPagesRouter) {
|
|
728
|
+
// Pages Router: Check for _app.tsx and _document.tsx
|
|
729
|
+
const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
|
|
730
|
+
|
|
731
|
+
const pagesRouterLayouts = [
|
|
732
|
+
`${pagesRoot}/_app.tsx`,
|
|
733
|
+
`${pagesRoot}/_app.jsx`,
|
|
734
|
+
`${pagesRoot}/_document.tsx`,
|
|
735
|
+
`${pagesRoot}/_document.jsx`,
|
|
736
|
+
];
|
|
737
|
+
|
|
738
|
+
for (const layoutPath of pagesRouterLayouts) {
|
|
739
|
+
if (fs.existsSync(path.join(projectRoot, layoutPath))) {
|
|
740
|
+
layoutFiles.push(layoutPath);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return layoutFiles;
|
|
746
|
+
}
|
|
747
|
+
|
|
578
748
|
/**
|
|
579
749
|
* Gather context about the current page for AI analysis
|
|
750
|
+
* Uses recursive import resolution to build complete component graph
|
|
580
751
|
*/
|
|
581
752
|
function gatherPageContext(
|
|
582
753
|
pageRoute: string,
|
|
@@ -590,50 +761,102 @@ function gatherPageContext(
|
|
|
590
761
|
const pageFile = discoverPageFile(pageRoute, projectRoot);
|
|
591
762
|
let pageContent = "";
|
|
592
763
|
const componentSources: { path: string; content: string }[] = [];
|
|
764
|
+
const visited = new Set<string>();
|
|
593
765
|
|
|
594
766
|
if (pageFile) {
|
|
595
767
|
const fullPath = path.join(projectRoot, pageFile);
|
|
596
768
|
if (fs.existsSync(fullPath)) {
|
|
597
769
|
pageContent = fs.readFileSync(fullPath, "utf-8");
|
|
770
|
+
visited.add(pageFile);
|
|
598
771
|
|
|
772
|
+
// Recursively gather all imported components (up to 4 levels deep)
|
|
599
773
|
const imports = extractImports(pageContent);
|
|
600
774
|
for (const importPath of imports) {
|
|
601
775
|
const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
|
|
602
|
-
if (resolvedPath &&
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
776
|
+
if (resolvedPath && !visited.has(resolvedPath)) {
|
|
777
|
+
const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
|
|
778
|
+
componentSources.push(...nestedComponents);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Also include layout files
|
|
783
|
+
const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
|
|
784
|
+
for (const layoutFile of layoutFiles) {
|
|
785
|
+
if (!visited.has(layoutFile)) {
|
|
786
|
+
const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
|
|
787
|
+
componentSources.push(...layoutComponents);
|
|
609
788
|
}
|
|
610
789
|
}
|
|
611
790
|
}
|
|
612
791
|
}
|
|
613
792
|
|
|
614
793
|
let globalsCSS = "";
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
794
|
+
const globalsCSSPatterns = [
|
|
795
|
+
"src/app/globals.css",
|
|
796
|
+
"app/globals.css",
|
|
797
|
+
"src/styles/globals.css",
|
|
798
|
+
"styles/globals.css",
|
|
799
|
+
"src/styles/global.css",
|
|
800
|
+
"styles/global.css",
|
|
801
|
+
];
|
|
802
|
+
|
|
803
|
+
for (const cssPattern of globalsCSSPatterns) {
|
|
804
|
+
const globalsPath = path.join(projectRoot, cssPattern);
|
|
805
|
+
if (fs.existsSync(globalsPath)) {
|
|
806
|
+
globalsCSS = fs.readFileSync(globalsPath, "utf-8");
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
618
809
|
}
|
|
619
810
|
|
|
620
811
|
return { pageFile, pageContent, componentSources, globalsCSS };
|
|
621
812
|
}
|
|
622
813
|
|
|
623
814
|
function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
815
|
+
// Handle root route
|
|
624
816
|
if (route === "/" || route === "") {
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
817
|
+
const rootPatterns = [
|
|
818
|
+
// App Router patterns
|
|
819
|
+
"src/app/page.tsx",
|
|
820
|
+
"src/app/page.jsx",
|
|
821
|
+
"app/page.tsx",
|
|
822
|
+
"app/page.jsx",
|
|
823
|
+
// Pages Router patterns
|
|
824
|
+
"src/pages/index.tsx",
|
|
825
|
+
"src/pages/index.jsx",
|
|
826
|
+
"pages/index.tsx",
|
|
827
|
+
"pages/index.jsx",
|
|
828
|
+
];
|
|
829
|
+
|
|
830
|
+
for (const pattern of rootPatterns) {
|
|
831
|
+
if (fs.existsSync(path.join(projectRoot, pattern))) {
|
|
832
|
+
return pattern;
|
|
833
|
+
}
|
|
628
834
|
}
|
|
629
835
|
return null;
|
|
630
836
|
}
|
|
631
837
|
|
|
632
838
|
const cleanRoute = route.replace(/^\//, "");
|
|
839
|
+
|
|
840
|
+
// First, try exact match patterns
|
|
633
841
|
const patterns = [
|
|
842
|
+
// App Router patterns (with src)
|
|
634
843
|
`src/app/${cleanRoute}/page.tsx`,
|
|
635
844
|
`src/app/${cleanRoute}/page.jsx`,
|
|
636
|
-
|
|
845
|
+
// App Router patterns (without src)
|
|
846
|
+
`app/${cleanRoute}/page.tsx`,
|
|
847
|
+
`app/${cleanRoute}/page.jsx`,
|
|
848
|
+
// Pages Router patterns (with src) - file-based
|
|
849
|
+
`src/pages/${cleanRoute}.tsx`,
|
|
850
|
+
`src/pages/${cleanRoute}.jsx`,
|
|
851
|
+
// Pages Router patterns (with src) - folder-based
|
|
852
|
+
`src/pages/${cleanRoute}/index.tsx`,
|
|
853
|
+
`src/pages/${cleanRoute}/index.jsx`,
|
|
854
|
+
// Pages Router patterns (without src) - file-based
|
|
855
|
+
`pages/${cleanRoute}.tsx`,
|
|
856
|
+
`pages/${cleanRoute}.jsx`,
|
|
857
|
+
// Pages Router patterns (without src) - folder-based
|
|
858
|
+
`pages/${cleanRoute}/index.tsx`,
|
|
859
|
+
`pages/${cleanRoute}/index.jsx`,
|
|
637
860
|
];
|
|
638
861
|
|
|
639
862
|
for (const pattern of patterns) {
|
|
@@ -642,6 +865,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
|
642
865
|
}
|
|
643
866
|
}
|
|
644
867
|
|
|
868
|
+
// If exact match not found, try dynamic route matching
|
|
869
|
+
// e.g., /processes/123 -> src/pages/processes/[id].tsx
|
|
870
|
+
const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
|
|
871
|
+
if (dynamicResult) {
|
|
872
|
+
return dynamicResult;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Find dynamic route files when exact match fails
|
|
880
|
+
* Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
|
|
881
|
+
*/
|
|
882
|
+
function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
|
|
883
|
+
const segments = cleanRoute.split("/");
|
|
884
|
+
|
|
885
|
+
// Try replacing the last segment with dynamic patterns
|
|
886
|
+
const baseDirs = [
|
|
887
|
+
"src/app",
|
|
888
|
+
"app",
|
|
889
|
+
"src/pages",
|
|
890
|
+
"pages",
|
|
891
|
+
];
|
|
892
|
+
|
|
893
|
+
const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
|
|
894
|
+
const extensions = [".tsx", ".jsx"];
|
|
895
|
+
|
|
896
|
+
for (const baseDir of baseDirs) {
|
|
897
|
+
const basePath = path.join(projectRoot, baseDir);
|
|
898
|
+
if (!fs.existsSync(basePath)) continue;
|
|
899
|
+
|
|
900
|
+
// Build path with all segments except the last one
|
|
901
|
+
const parentSegments = segments.slice(0, -1);
|
|
902
|
+
const parentPath = parentSegments.length > 0
|
|
903
|
+
? path.join(basePath, ...parentSegments)
|
|
904
|
+
: basePath;
|
|
905
|
+
|
|
906
|
+
if (!fs.existsSync(parentPath)) continue;
|
|
907
|
+
|
|
908
|
+
// Check for dynamic route files
|
|
909
|
+
for (const dynPattern of dynamicPatterns) {
|
|
910
|
+
for (const ext of extensions) {
|
|
911
|
+
// App Router: parent/[id]/page.tsx
|
|
912
|
+
if (baseDir.includes("app")) {
|
|
913
|
+
const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
|
|
914
|
+
if (fs.existsSync(appRouterPath)) {
|
|
915
|
+
return path.relative(projectRoot, appRouterPath);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Pages Router: parent/[id].tsx
|
|
920
|
+
const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
|
|
921
|
+
if (fs.existsSync(pagesRouterFile)) {
|
|
922
|
+
return path.relative(projectRoot, pagesRouterFile);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Pages Router: parent/[id]/index.tsx
|
|
926
|
+
const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
|
|
927
|
+
if (fs.existsSync(pagesRouterDir)) {
|
|
928
|
+
return path.relative(projectRoot, pagesRouterDir);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Also scan directory for any dynamic segment pattern [...]
|
|
934
|
+
try {
|
|
935
|
+
const entries = fs.readdirSync(parentPath, { withFileTypes: true });
|
|
936
|
+
for (const entry of entries) {
|
|
937
|
+
if (entry.name.startsWith("[") && entry.name.includes("]")) {
|
|
938
|
+
for (const ext of extensions) {
|
|
939
|
+
if (entry.isDirectory()) {
|
|
940
|
+
// App Router or Pages Router with folder
|
|
941
|
+
const pagePath = path.join(parentPath, entry.name, `page${ext}`);
|
|
942
|
+
const indexPath = path.join(parentPath, entry.name, `index${ext}`);
|
|
943
|
+
if (fs.existsSync(pagePath)) {
|
|
944
|
+
return path.relative(projectRoot, pagePath);
|
|
945
|
+
}
|
|
946
|
+
if (fs.existsSync(indexPath)) {
|
|
947
|
+
return path.relative(projectRoot, indexPath);
|
|
948
|
+
}
|
|
949
|
+
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
950
|
+
// Pages Router file-based
|
|
951
|
+
return path.relative(projectRoot, path.join(parentPath, entry.name));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
} catch {
|
|
957
|
+
// Skip if directory can't be read
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
645
961
|
return null;
|
|
646
962
|
}
|
|
647
963
|
|
|
@@ -709,6 +1025,76 @@ function generateSimpleDiff(original: string, modified: string): string {
|
|
|
709
1025
|
return diff.join("\n");
|
|
710
1026
|
}
|
|
711
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Patch interface for search/replace operations
|
|
1030
|
+
*/
|
|
1031
|
+
interface Patch {
|
|
1032
|
+
search: string;
|
|
1033
|
+
replace: string;
|
|
1034
|
+
explanation: string;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Result of applying patches to a file
|
|
1039
|
+
*/
|
|
1040
|
+
interface ApplyPatchesResult {
|
|
1041
|
+
success: boolean;
|
|
1042
|
+
modifiedContent: string;
|
|
1043
|
+
appliedPatches: number;
|
|
1044
|
+
failedPatches: { patch: Patch; error: string }[];
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Apply search/replace patches to file content
|
|
1049
|
+
* This is the core of the patch-based editing system
|
|
1050
|
+
*/
|
|
1051
|
+
function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
|
|
1052
|
+
let content = originalContent;
|
|
1053
|
+
let appliedPatches = 0;
|
|
1054
|
+
const failedPatches: { patch: Patch; error: string }[] = [];
|
|
1055
|
+
|
|
1056
|
+
for (const patch of patches) {
|
|
1057
|
+
// Normalize line endings for matching
|
|
1058
|
+
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1059
|
+
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1060
|
+
|
|
1061
|
+
// Check if search string exists in content
|
|
1062
|
+
if (!content.includes(normalizedSearch)) {
|
|
1063
|
+
// Try with different whitespace normalization
|
|
1064
|
+
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1065
|
+
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1066
|
+
|
|
1067
|
+
if (!regex.test(content)) {
|
|
1068
|
+
failedPatches.push({
|
|
1069
|
+
patch,
|
|
1070
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1071
|
+
});
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// If regex matched, use regex replace
|
|
1076
|
+
content = content.replace(regex, normalizedReplace);
|
|
1077
|
+
appliedPatches++;
|
|
1078
|
+
} else {
|
|
1079
|
+
// Exact match found - apply the replacement
|
|
1080
|
+
// Only replace the first occurrence to be safe
|
|
1081
|
+
const index = content.indexOf(normalizedSearch);
|
|
1082
|
+
content =
|
|
1083
|
+
content.substring(0, index) +
|
|
1084
|
+
normalizedReplace +
|
|
1085
|
+
content.substring(index + normalizedSearch.length);
|
|
1086
|
+
appliedPatches++;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return {
|
|
1091
|
+
success: failedPatches.length === 0,
|
|
1092
|
+
modifiedContent: content,
|
|
1093
|
+
appliedPatches,
|
|
1094
|
+
failedPatches,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
712
1098
|
/**
|
|
713
1099
|
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
714
1100
|
*/
|