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.
|
@@ -56,72 +56,65 @@ interface VisionEditResponse {
|
|
|
56
56
|
error?: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const VISION_SYSTEM_PROMPT = `You are
|
|
59
|
+
const VISION_SYSTEM_PROMPT = `You are an expert React/TypeScript developer with vision capabilities. You can see screenshots and modify code.
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
UNDERSTAND THE USER'S REQUEST NATURALLY
|
|
63
|
+
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
Read the user's request and do exactly what they ask. You know React, TypeScript, Tailwind CSS, and JSX - use that knowledge naturally.
|
|
66
|
+
|
|
67
|
+
- If they say "add X", ADD X to the code (don't replace or remove anything else)
|
|
68
|
+
- If they say "change X to Y", find X and change it to Y
|
|
69
|
+
- If they say "make X bigger/smaller/different color", adjust the relevant properties
|
|
70
|
+
- If they say "remove X", remove X
|
|
71
|
+
- If they say "wrap X with Y", add Y as a parent around X
|
|
72
|
+
|
|
73
|
+
For any change:
|
|
74
|
+
1. Read the code context provided
|
|
75
|
+
2. Understand what the user wants
|
|
76
|
+
3. Generate patches that accomplish exactly that
|
|
77
|
+
4. If adding a component/icon requires an import, include a patch for the import too
|
|
78
|
+
|
|
79
|
+
Don't overthink - just make the change the user requested.
|
|
62
80
|
|
|
63
81
|
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
-
|
|
82
|
+
PATCH FORMAT
|
|
65
83
|
═══════════════════════════════════════════════════════════════════════════════
|
|
66
84
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
**FILE RULES:**
|
|
75
|
-
6. You may ONLY edit files that are provided in the PAGE CONTEXT section
|
|
76
|
-
7. NEVER create new files - only modify existing ones shown to you
|
|
77
|
-
8. The filePath in your response MUST exactly match one of the provided file paths
|
|
78
|
-
9. If you cannot find the right file to edit, explain this in your response instead of creating a new file
|
|
79
|
-
|
|
80
|
-
**PRESERVATION RULES (ZERO TOLERANCE):**
|
|
81
|
-
10. NEVER delete or remove existing functions, hooks, state, or logic
|
|
82
|
-
11. NEVER remove imports - you may only ADD imports if needed
|
|
83
|
-
12. NEVER remove useEffect, useState, useCallback, or other React hooks
|
|
84
|
-
13. NEVER remove API calls, fetch requests, or data loading logic
|
|
85
|
-
14. NEVER remove error handling, loading states, or conditional rendering
|
|
86
|
-
15. NEVER remove data-sonance-* attributes
|
|
87
|
-
16. NEVER change component structure unless specifically requested
|
|
88
|
-
17. NEVER modify TypeScript types or interfaces unless specifically requested
|
|
89
|
-
|
|
90
|
-
**CHANGE RULES:**
|
|
91
|
-
18. Make ONLY the changes requested by the user
|
|
92
|
-
19. Modify the MINIMUM amount of code necessary
|
|
93
|
-
20. Keep all existing className values and ADD to them if needed
|
|
94
|
-
21. Use semantic Tailwind classes (bg-primary, text-foreground, etc.)
|
|
95
|
-
22. Maintain dark mode compatibility with CSS variables
|
|
96
|
-
23. Keep the cn() utility for className merging
|
|
97
|
-
24. 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.
|
|
85
|
+
Return search/replace patches (NOT full files). The system applies your patches to the original.
|
|
86
|
+
|
|
87
|
+
**Patch Rules:**
|
|
88
|
+
- "search" must match the original code EXACTLY (including whitespace/indentation)
|
|
89
|
+
- "replace" contains your modified version
|
|
90
|
+
- Include 2-4 lines of context in "search" to make it unique
|
|
91
|
+
- You may ONLY edit files provided in the PAGE CONTEXT section
|
|
98
92
|
|
|
99
93
|
**SONANCE BRAND COLORS:**
|
|
100
|
-
- Charcoal: #333F48
|
|
101
|
-
- Silver: #E2E2E2, #D1D1D6 (secondary)
|
|
94
|
+
- Charcoal: #333F48 (primary text)
|
|
102
95
|
- IPORT Orange: #FC4C02
|
|
103
|
-
- IPORT Dark: #0F161D
|
|
104
96
|
- Blaze Blue: #00A3E1
|
|
105
|
-
- Blaze Red: #C02B0A
|
|
106
97
|
|
|
107
98
|
**RESPONSE FORMAT:**
|
|
108
|
-
Return ONLY
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
99
|
+
Return ONLY valid JSON:
|
|
100
|
+
{
|
|
101
|
+
"reasoning": "What you understood from the request and your plan",
|
|
102
|
+
"modifications": [
|
|
103
|
+
{
|
|
104
|
+
"filePath": "path/to/file.tsx",
|
|
105
|
+
"patches": [
|
|
106
|
+
{
|
|
107
|
+
"search": "exact code to find",
|
|
108
|
+
"replace": "the replacement code",
|
|
109
|
+
"explanation": "what this patch does"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"previewCSS": "optional CSS for live preview"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"aggregatedPreviewCSS": "combined CSS for all changes",
|
|
116
|
+
"explanation": "summary of changes made"
|
|
117
|
+
}`;
|
|
125
118
|
|
|
126
119
|
export async function POST(request: Request) {
|
|
127
120
|
// Only allow in development
|
|
@@ -250,14 +243,38 @@ ${pageContext.pageContent ? `\`\`\`tsx\n${pageContext.pageContent}\n\`\`\`` : ""
|
|
|
250
243
|
`;
|
|
251
244
|
|
|
252
245
|
if (pageContext.componentSources.length > 0) {
|
|
253
|
-
|
|
254
|
-
|
|
246
|
+
// Smart truncation: prioritize first components (direct imports) and limit total context
|
|
247
|
+
const MAX_TOTAL_CONTEXT = 80000; // ~80k chars to stay well under Claude's limit
|
|
248
|
+
const MAX_PER_FILE_PRIORITY = 4000; // First 10 files get more space
|
|
249
|
+
const MAX_PER_FILE_SECONDARY = 1500; // Remaining files get less
|
|
250
|
+
const MAX_FILES = 30; // Limit total number of files
|
|
251
|
+
|
|
252
|
+
let usedContext = pageContext.pageContent.length + pageContext.globalsCSS.length;
|
|
253
|
+
const truncatedComponents = pageContext.componentSources.slice(0, MAX_FILES);
|
|
254
|
+
|
|
255
|
+
textContent += `IMPORTED COMPONENTS (${truncatedComponents.length} files, ${pageContext.componentSources.length > MAX_FILES ? `${pageContext.componentSources.length - MAX_FILES} omitted` : 'complete'}):\n`;
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < truncatedComponents.length; i++) {
|
|
258
|
+
const comp = truncatedComponents[i];
|
|
259
|
+
const isPriority = i < 10; // First 10 files are priority (direct imports)
|
|
260
|
+
const maxSize = isPriority ? MAX_PER_FILE_PRIORITY : MAX_PER_FILE_SECONDARY;
|
|
261
|
+
|
|
262
|
+
// Stop if we've used too much context
|
|
263
|
+
if (usedContext > MAX_TOTAL_CONTEXT) {
|
|
264
|
+
textContent += `\n// ... (${truncatedComponents.length - i} more files omitted to stay within context limits)\n`;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const truncatedContent = comp.content.substring(0, maxSize);
|
|
269
|
+
const wasTruncated = comp.content.length > maxSize;
|
|
270
|
+
|
|
255
271
|
textContent += `
|
|
256
|
-
File: ${comp.path}
|
|
272
|
+
File: ${comp.path}${isPriority ? '' : ' (nested)'}
|
|
257
273
|
\`\`\`tsx
|
|
258
|
-
${
|
|
274
|
+
${truncatedContent}${wasTruncated ? "\n// ... (truncated)" : ""}
|
|
259
275
|
\`\`\`
|
|
260
276
|
`;
|
|
277
|
+
usedContext += truncatedContent.length;
|
|
261
278
|
}
|
|
262
279
|
}
|
|
263
280
|
|
|
@@ -310,13 +327,15 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
310
327
|
);
|
|
311
328
|
}
|
|
312
329
|
|
|
313
|
-
// Parse AI response
|
|
330
|
+
// Parse AI response - now expecting patches instead of full file content
|
|
314
331
|
let aiResponse: {
|
|
315
332
|
reasoning?: string;
|
|
316
333
|
modifications: Array<{
|
|
317
334
|
filePath: string;
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
patches?: Patch[];
|
|
336
|
+
// Legacy support for modifiedContent (will be deprecated)
|
|
337
|
+
modifiedContent?: string;
|
|
338
|
+
explanation?: string;
|
|
320
339
|
previewCSS?: string;
|
|
321
340
|
}>;
|
|
322
341
|
aggregatedPreviewCSS?: string;
|
|
@@ -381,10 +400,9 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
381
400
|
);
|
|
382
401
|
}
|
|
383
402
|
|
|
384
|
-
//
|
|
403
|
+
// Process modifications - apply patches to get modified content
|
|
385
404
|
const modificationsWithOriginals: VisionFileModification[] = [];
|
|
386
|
-
const
|
|
387
|
-
const validationWarnings: string[] = [];
|
|
405
|
+
const patchErrors: string[] = [];
|
|
388
406
|
|
|
389
407
|
for (const mod of aiResponse.modifications || []) {
|
|
390
408
|
const fullPath = path.join(projectRoot, mod.filePath);
|
|
@@ -393,43 +411,76 @@ CRITICAL: Only use file paths from the VALID FILES list above. Do NOT create new
|
|
|
393
411
|
originalContent = fs.readFileSync(fullPath, "utf-8");
|
|
394
412
|
}
|
|
395
413
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
414
|
+
let modifiedContent: string;
|
|
415
|
+
let explanation = mod.explanation || "";
|
|
416
|
+
|
|
417
|
+
// Check if AI returned patches (new format) or modifiedContent (legacy)
|
|
418
|
+
if (mod.patches && mod.patches.length > 0) {
|
|
419
|
+
// New patch-based approach
|
|
420
|
+
console.log(`[Vision Mode] Applying ${mod.patches.length} patches to ${mod.filePath}`);
|
|
421
|
+
|
|
422
|
+
const patchResult = applyPatches(originalContent, mod.patches);
|
|
423
|
+
|
|
424
|
+
if (!patchResult.success) {
|
|
425
|
+
const failedMessages = patchResult.failedPatches.map(
|
|
426
|
+
(f) => `Patch failed: ${f.error}`
|
|
427
|
+
).join("\n");
|
|
428
|
+
patchErrors.push(`${mod.filePath}:\n${failedMessages}`);
|
|
429
|
+
|
|
430
|
+
// If some patches succeeded, use partial result
|
|
431
|
+
if (patchResult.appliedPatches > 0) {
|
|
432
|
+
console.warn(`[Vision Mode] ${patchResult.appliedPatches}/${mod.patches.length} patches applied to ${mod.filePath}`);
|
|
433
|
+
modifiedContent = patchResult.modifiedContent;
|
|
434
|
+
explanation += ` (${patchResult.appliedPatches}/${mod.patches.length} patches applied)`;
|
|
435
|
+
} else {
|
|
436
|
+
continue; // Skip this file entirely if no patches worked
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
modifiedContent = patchResult.modifiedContent;
|
|
440
|
+
console.log(`[Vision Mode] All ${mod.patches.length} patches applied successfully to ${mod.filePath}`);
|
|
441
|
+
}
|
|
442
|
+
} else if (mod.modifiedContent) {
|
|
443
|
+
// Legacy: AI returned full file content
|
|
444
|
+
console.warn(`[Vision Mode] Legacy modifiedContent received for ${mod.filePath} - patch-based format preferred`);
|
|
445
|
+
modifiedContent = mod.modifiedContent;
|
|
446
|
+
|
|
447
|
+
// Validate the modification using legacy validation
|
|
448
|
+
const validation = validateModification(originalContent, modifiedContent, mod.filePath);
|
|
449
|
+
if (!validation.valid) {
|
|
450
|
+
patchErrors.push(`${mod.filePath}: ${validation.error}`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// No patches and no modifiedContent - skip
|
|
455
|
+
console.warn(`[Vision Mode] No patches or modifiedContent for ${mod.filePath}`);
|
|
456
|
+
continue;
|
|
406
457
|
}
|
|
407
458
|
|
|
408
459
|
modificationsWithOriginals.push({
|
|
409
460
|
filePath: mod.filePath,
|
|
410
461
|
originalContent,
|
|
411
|
-
modifiedContent
|
|
412
|
-
diff: generateSimpleDiff(originalContent,
|
|
413
|
-
explanation
|
|
462
|
+
modifiedContent,
|
|
463
|
+
diff: generateSimpleDiff(originalContent, modifiedContent),
|
|
464
|
+
explanation,
|
|
414
465
|
previewCSS: mod.previewCSS,
|
|
415
466
|
});
|
|
416
467
|
}
|
|
417
468
|
|
|
418
|
-
// If all modifications failed
|
|
419
|
-
if (
|
|
420
|
-
console.error("All AI
|
|
469
|
+
// If all modifications failed, return error
|
|
470
|
+
if (patchErrors.length > 0 && modificationsWithOriginals.length === 0) {
|
|
471
|
+
console.error("All AI patches failed:", patchErrors);
|
|
421
472
|
return NextResponse.json(
|
|
422
473
|
{
|
|
423
474
|
success: false,
|
|
424
|
-
error:
|
|
475
|
+
error: `Patch application failed:\n\n${patchErrors.join("\n\n")}`,
|
|
425
476
|
} as VisionEditResponse,
|
|
426
477
|
{ status: 400 }
|
|
427
478
|
);
|
|
428
479
|
}
|
|
429
480
|
|
|
430
|
-
// Log warnings
|
|
431
|
-
if (
|
|
432
|
-
console.warn("
|
|
481
|
+
// Log patch errors as warnings if some modifications succeeded
|
|
482
|
+
if (patchErrors.length > 0) {
|
|
483
|
+
console.warn("Some patches failed:", patchErrors);
|
|
433
484
|
}
|
|
434
485
|
|
|
435
486
|
// Aggregate preview CSS
|
|
@@ -524,8 +575,109 @@ function findComponentFileByName(
|
|
|
524
575
|
return null;
|
|
525
576
|
}
|
|
526
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Recursively gather all imports from a file up to a max depth
|
|
580
|
+
* This builds a complete component graph for the AI to understand
|
|
581
|
+
*/
|
|
582
|
+
function gatherAllImports(
|
|
583
|
+
filePath: string,
|
|
584
|
+
projectRoot: string,
|
|
585
|
+
visited: Set<string> = new Set(),
|
|
586
|
+
maxDepth: number = 4
|
|
587
|
+
): { path: string; content: string }[] {
|
|
588
|
+
// Prevent infinite loops and limit total files
|
|
589
|
+
if (visited.has(filePath) || visited.size > 50) return [];
|
|
590
|
+
visited.add(filePath);
|
|
591
|
+
|
|
592
|
+
const results: { path: string; content: string }[] = [];
|
|
593
|
+
const fullPath = path.join(projectRoot, filePath);
|
|
594
|
+
|
|
595
|
+
if (!fs.existsSync(fullPath)) return results;
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
599
|
+
results.push({ path: filePath, content });
|
|
600
|
+
|
|
601
|
+
// Continue recursing if we haven't hit max depth
|
|
602
|
+
if (maxDepth > 0) {
|
|
603
|
+
const imports = extractImports(content);
|
|
604
|
+
for (const imp of imports) {
|
|
605
|
+
const resolved = resolveImportPath(imp, filePath, projectRoot);
|
|
606
|
+
if (resolved && !visited.has(resolved)) {
|
|
607
|
+
const nestedImports = gatherAllImports(resolved, projectRoot, visited, maxDepth - 1);
|
|
608
|
+
results.push(...nestedImports);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
// Skip files that can't be read
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return results;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Discover layout files that wrap the page
|
|
621
|
+
* App Router: layout.tsx in same and parent directories
|
|
622
|
+
* Pages Router: _app.tsx and _document.tsx
|
|
623
|
+
*/
|
|
624
|
+
function discoverLayoutFiles(pageFile: string | null, projectRoot: string): string[] {
|
|
625
|
+
const layoutFiles: string[] = [];
|
|
626
|
+
|
|
627
|
+
if (!pageFile) return layoutFiles;
|
|
628
|
+
|
|
629
|
+
// Determine if App Router or Pages Router
|
|
630
|
+
const isAppRouter = pageFile.includes("/app/") || pageFile.startsWith("app/");
|
|
631
|
+
const isPagesRouter = pageFile.includes("/pages/") || pageFile.startsWith("pages/");
|
|
632
|
+
|
|
633
|
+
if (isAppRouter) {
|
|
634
|
+
// App Router: Check for layout.tsx in same directory and parent directories
|
|
635
|
+
let currentDir = path.dirname(pageFile);
|
|
636
|
+
const appRoot = pageFile.includes("src/app") ? "src/app" : "app";
|
|
637
|
+
|
|
638
|
+
while (currentDir.includes(appRoot)) {
|
|
639
|
+
const layoutPatterns = [
|
|
640
|
+
path.join(currentDir, "layout.tsx"),
|
|
641
|
+
path.join(currentDir, "layout.jsx"),
|
|
642
|
+
];
|
|
643
|
+
|
|
644
|
+
for (const layoutPath of layoutPatterns) {
|
|
645
|
+
if (fs.existsSync(path.join(projectRoot, layoutPath))) {
|
|
646
|
+
layoutFiles.push(layoutPath);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Move to parent directory
|
|
651
|
+
const parentDir = path.dirname(currentDir);
|
|
652
|
+
if (parentDir === currentDir) break;
|
|
653
|
+
currentDir = parentDir;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (isPagesRouter) {
|
|
658
|
+
// Pages Router: Check for _app.tsx and _document.tsx
|
|
659
|
+
const pagesRoot = pageFile.includes("src/pages") ? "src/pages" : "pages";
|
|
660
|
+
|
|
661
|
+
const pagesRouterLayouts = [
|
|
662
|
+
`${pagesRoot}/_app.tsx`,
|
|
663
|
+
`${pagesRoot}/_app.jsx`,
|
|
664
|
+
`${pagesRoot}/_document.tsx`,
|
|
665
|
+
`${pagesRoot}/_document.jsx`,
|
|
666
|
+
];
|
|
667
|
+
|
|
668
|
+
for (const layoutPath of pagesRouterLayouts) {
|
|
669
|
+
if (fs.existsSync(path.join(projectRoot, layoutPath))) {
|
|
670
|
+
layoutFiles.push(layoutPath);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return layoutFiles;
|
|
676
|
+
}
|
|
677
|
+
|
|
527
678
|
/**
|
|
528
679
|
* Gather context about the current page for AI analysis
|
|
680
|
+
* Uses recursive import resolution to build complete component graph
|
|
529
681
|
*/
|
|
530
682
|
function gatherPageContext(
|
|
531
683
|
pageRoute: string,
|
|
@@ -541,23 +693,30 @@ function gatherPageContext(
|
|
|
541
693
|
const pageFile = discoverPageFile(pageRoute, projectRoot);
|
|
542
694
|
let pageContent = "";
|
|
543
695
|
const componentSources: { path: string; content: string }[] = [];
|
|
696
|
+
const visited = new Set<string>();
|
|
544
697
|
|
|
545
698
|
if (pageFile) {
|
|
546
699
|
const fullPath = path.join(projectRoot, pageFile);
|
|
547
700
|
if (fs.existsSync(fullPath)) {
|
|
548
701
|
pageContent = fs.readFileSync(fullPath, "utf-8");
|
|
702
|
+
visited.add(pageFile);
|
|
549
703
|
|
|
550
|
-
//
|
|
704
|
+
// Recursively gather all imported components (up to 4 levels deep)
|
|
551
705
|
const imports = extractImports(pageContent);
|
|
552
706
|
for (const importPath of imports) {
|
|
553
707
|
const resolvedPath = resolveImportPath(importPath, pageFile, projectRoot);
|
|
554
|
-
if (resolvedPath &&
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
708
|
+
if (resolvedPath && !visited.has(resolvedPath)) {
|
|
709
|
+
const nestedComponents = gatherAllImports(resolvedPath, projectRoot, visited, 3);
|
|
710
|
+
componentSources.push(...nestedComponents);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Also include layout files
|
|
715
|
+
const layoutFiles = discoverLayoutFiles(pageFile, projectRoot);
|
|
716
|
+
for (const layoutFile of layoutFiles) {
|
|
717
|
+
if (!visited.has(layoutFile)) {
|
|
718
|
+
const layoutComponents = gatherAllImports(layoutFile, projectRoot, visited, 2);
|
|
719
|
+
componentSources.push(...layoutComponents);
|
|
561
720
|
}
|
|
562
721
|
}
|
|
563
722
|
}
|
|
@@ -568,25 +727,30 @@ function gatherPageContext(
|
|
|
568
727
|
for (const el of focusedElements) {
|
|
569
728
|
// Try to find component file by name
|
|
570
729
|
const foundPath = findComponentFileByName(el.name, projectRoot);
|
|
571
|
-
if (foundPath && !componentSources.some((c) => c.path === foundPath)) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
path.join(projectRoot, foundPath),
|
|
575
|
-
"utf-8"
|
|
576
|
-
);
|
|
577
|
-
componentSources.push({ path: foundPath, content });
|
|
578
|
-
} catch {
|
|
579
|
-
/* skip if unreadable */
|
|
580
|
-
}
|
|
730
|
+
if (foundPath && !visited.has(foundPath) && !componentSources.some((c) => c.path === foundPath)) {
|
|
731
|
+
const focusedComponents = gatherAllImports(foundPath, projectRoot, visited, 2);
|
|
732
|
+
componentSources.push(...focusedComponents);
|
|
581
733
|
}
|
|
582
734
|
}
|
|
583
735
|
}
|
|
584
736
|
|
|
585
|
-
// Read globals.css
|
|
737
|
+
// Read globals.css - check multiple possible locations
|
|
586
738
|
let globalsCSS = "";
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
739
|
+
const globalsCSSPatterns = [
|
|
740
|
+
"src/app/globals.css",
|
|
741
|
+
"app/globals.css",
|
|
742
|
+
"src/styles/globals.css",
|
|
743
|
+
"styles/globals.css",
|
|
744
|
+
"src/styles/global.css",
|
|
745
|
+
"styles/global.css",
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
for (const cssPattern of globalsCSSPatterns) {
|
|
749
|
+
const globalsPath = path.join(projectRoot, cssPattern);
|
|
750
|
+
if (fs.existsSync(globalsPath)) {
|
|
751
|
+
globalsCSS = fs.readFileSync(globalsPath, "utf-8");
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
590
754
|
}
|
|
591
755
|
|
|
592
756
|
return { pageFile, pageContent, componentSources, globalsCSS };
|
|
@@ -594,13 +758,28 @@ function gatherPageContext(
|
|
|
594
758
|
|
|
595
759
|
/**
|
|
596
760
|
* Discover the page file for a given route
|
|
761
|
+
* Supports both App Router (src/app/) and Pages Router (src/pages/, pages/)
|
|
597
762
|
*/
|
|
598
763
|
function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
599
764
|
// Handle root route
|
|
600
765
|
if (route === "/" || route === "") {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
766
|
+
const rootPatterns = [
|
|
767
|
+
// App Router patterns
|
|
768
|
+
"src/app/page.tsx",
|
|
769
|
+
"src/app/page.jsx",
|
|
770
|
+
"app/page.tsx",
|
|
771
|
+
"app/page.jsx",
|
|
772
|
+
// Pages Router patterns
|
|
773
|
+
"src/pages/index.tsx",
|
|
774
|
+
"src/pages/index.jsx",
|
|
775
|
+
"pages/index.tsx",
|
|
776
|
+
"pages/index.jsx",
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
for (const pattern of rootPatterns) {
|
|
780
|
+
if (fs.existsSync(path.join(projectRoot, pattern))) {
|
|
781
|
+
return pattern;
|
|
782
|
+
}
|
|
604
783
|
}
|
|
605
784
|
return null;
|
|
606
785
|
}
|
|
@@ -608,11 +787,26 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
|
608
787
|
// Remove leading slash
|
|
609
788
|
const cleanRoute = route.replace(/^\//, "");
|
|
610
789
|
|
|
611
|
-
//
|
|
790
|
+
// First, try exact match patterns
|
|
612
791
|
const patterns = [
|
|
792
|
+
// App Router patterns (with src)
|
|
613
793
|
`src/app/${cleanRoute}/page.tsx`,
|
|
614
794
|
`src/app/${cleanRoute}/page.jsx`,
|
|
615
|
-
|
|
795
|
+
// App Router patterns (without src)
|
|
796
|
+
`app/${cleanRoute}/page.tsx`,
|
|
797
|
+
`app/${cleanRoute}/page.jsx`,
|
|
798
|
+
// Pages Router patterns (with src) - file-based
|
|
799
|
+
`src/pages/${cleanRoute}.tsx`,
|
|
800
|
+
`src/pages/${cleanRoute}.jsx`,
|
|
801
|
+
// Pages Router patterns (with src) - folder-based
|
|
802
|
+
`src/pages/${cleanRoute}/index.tsx`,
|
|
803
|
+
`src/pages/${cleanRoute}/index.jsx`,
|
|
804
|
+
// Pages Router patterns (without src) - file-based
|
|
805
|
+
`pages/${cleanRoute}.tsx`,
|
|
806
|
+
`pages/${cleanRoute}.jsx`,
|
|
807
|
+
// Pages Router patterns (without src) - folder-based
|
|
808
|
+
`pages/${cleanRoute}/index.tsx`,
|
|
809
|
+
`pages/${cleanRoute}/index.jsx`,
|
|
616
810
|
];
|
|
617
811
|
|
|
618
812
|
for (const pattern of patterns) {
|
|
@@ -621,6 +815,99 @@ function discoverPageFile(route: string, projectRoot: string): string | null {
|
|
|
621
815
|
}
|
|
622
816
|
}
|
|
623
817
|
|
|
818
|
+
// If exact match not found, try dynamic route matching
|
|
819
|
+
// e.g., /processes/123 -> src/pages/processes/[id].tsx
|
|
820
|
+
const dynamicResult = findDynamicRoute(cleanRoute, projectRoot);
|
|
821
|
+
if (dynamicResult) {
|
|
822
|
+
return dynamicResult;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Find dynamic route files when exact match fails
|
|
830
|
+
* Maps runtime routes like "/processes/123" to file paths like "src/pages/processes/[id].tsx"
|
|
831
|
+
*/
|
|
832
|
+
function findDynamicRoute(cleanRoute: string, projectRoot: string): string | null {
|
|
833
|
+
const segments = cleanRoute.split("/");
|
|
834
|
+
|
|
835
|
+
// Try replacing the last segment with dynamic patterns
|
|
836
|
+
const baseDirs = [
|
|
837
|
+
"src/app",
|
|
838
|
+
"app",
|
|
839
|
+
"src/pages",
|
|
840
|
+
"pages",
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
const dynamicPatterns = ["[id]", "[slug]", "[...slug]", "[[...slug]]"];
|
|
844
|
+
const extensions = [".tsx", ".jsx"];
|
|
845
|
+
|
|
846
|
+
for (const baseDir of baseDirs) {
|
|
847
|
+
const basePath = path.join(projectRoot, baseDir);
|
|
848
|
+
if (!fs.existsSync(basePath)) continue;
|
|
849
|
+
|
|
850
|
+
// Build path with all segments except the last one
|
|
851
|
+
const parentSegments = segments.slice(0, -1);
|
|
852
|
+
const parentPath = parentSegments.length > 0
|
|
853
|
+
? path.join(basePath, ...parentSegments)
|
|
854
|
+
: basePath;
|
|
855
|
+
|
|
856
|
+
if (!fs.existsSync(parentPath)) continue;
|
|
857
|
+
|
|
858
|
+
// Check for dynamic route files
|
|
859
|
+
for (const dynPattern of dynamicPatterns) {
|
|
860
|
+
for (const ext of extensions) {
|
|
861
|
+
// App Router: parent/[id]/page.tsx
|
|
862
|
+
if (baseDir.includes("app")) {
|
|
863
|
+
const appRouterPath = path.join(parentPath, dynPattern, `page${ext}`);
|
|
864
|
+
if (fs.existsSync(appRouterPath)) {
|
|
865
|
+
return path.relative(projectRoot, appRouterPath);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Pages Router: parent/[id].tsx
|
|
870
|
+
const pagesRouterFile = path.join(parentPath, `${dynPattern}${ext}`);
|
|
871
|
+
if (fs.existsSync(pagesRouterFile)) {
|
|
872
|
+
return path.relative(projectRoot, pagesRouterFile);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Pages Router: parent/[id]/index.tsx
|
|
876
|
+
const pagesRouterDir = path.join(parentPath, dynPattern, `index${ext}`);
|
|
877
|
+
if (fs.existsSync(pagesRouterDir)) {
|
|
878
|
+
return path.relative(projectRoot, pagesRouterDir);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Also scan directory for any dynamic segment pattern [...]
|
|
884
|
+
try {
|
|
885
|
+
const entries = fs.readdirSync(parentPath, { withFileTypes: true });
|
|
886
|
+
for (const entry of entries) {
|
|
887
|
+
if (entry.name.startsWith("[") && entry.name.includes("]")) {
|
|
888
|
+
for (const ext of extensions) {
|
|
889
|
+
if (entry.isDirectory()) {
|
|
890
|
+
// App Router or Pages Router with folder
|
|
891
|
+
const pagePath = path.join(parentPath, entry.name, `page${ext}`);
|
|
892
|
+
const indexPath = path.join(parentPath, entry.name, `index${ext}`);
|
|
893
|
+
if (fs.existsSync(pagePath)) {
|
|
894
|
+
return path.relative(projectRoot, pagePath);
|
|
895
|
+
}
|
|
896
|
+
if (fs.existsSync(indexPath)) {
|
|
897
|
+
return path.relative(projectRoot, indexPath);
|
|
898
|
+
}
|
|
899
|
+
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
900
|
+
// Pages Router file-based
|
|
901
|
+
return path.relative(projectRoot, path.join(parentPath, entry.name));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
} catch {
|
|
907
|
+
// Skip if directory can't be read
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
624
911
|
return null;
|
|
625
912
|
}
|
|
626
913
|
|
|
@@ -701,6 +988,76 @@ function generateSimpleDiff(original: string, modified: string): string {
|
|
|
701
988
|
return diff.join("\n");
|
|
702
989
|
}
|
|
703
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Patch interface for search/replace operations
|
|
993
|
+
*/
|
|
994
|
+
interface Patch {
|
|
995
|
+
search: string;
|
|
996
|
+
replace: string;
|
|
997
|
+
explanation: string;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Result of applying patches to a file
|
|
1002
|
+
*/
|
|
1003
|
+
interface ApplyPatchesResult {
|
|
1004
|
+
success: boolean;
|
|
1005
|
+
modifiedContent: string;
|
|
1006
|
+
appliedPatches: number;
|
|
1007
|
+
failedPatches: { patch: Patch; error: string }[];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Apply search/replace patches to file content
|
|
1012
|
+
* This is the core of the patch-based editing system
|
|
1013
|
+
*/
|
|
1014
|
+
function applyPatches(originalContent: string, patches: Patch[]): ApplyPatchesResult {
|
|
1015
|
+
let content = originalContent;
|
|
1016
|
+
let appliedPatches = 0;
|
|
1017
|
+
const failedPatches: { patch: Patch; error: string }[] = [];
|
|
1018
|
+
|
|
1019
|
+
for (const patch of patches) {
|
|
1020
|
+
// Normalize line endings for matching
|
|
1021
|
+
const normalizedSearch = patch.search.replace(/\\n/g, "\n");
|
|
1022
|
+
const normalizedReplace = patch.replace.replace(/\\n/g, "\n");
|
|
1023
|
+
|
|
1024
|
+
// Check if search string exists in content
|
|
1025
|
+
if (!content.includes(normalizedSearch)) {
|
|
1026
|
+
// Try with different whitespace normalization
|
|
1027
|
+
const flexibleSearch = normalizedSearch.replace(/\s+/g, "\\s+");
|
|
1028
|
+
const regex = new RegExp(flexibleSearch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\s\+/g, "\\s+"));
|
|
1029
|
+
|
|
1030
|
+
if (!regex.test(content)) {
|
|
1031
|
+
failedPatches.push({
|
|
1032
|
+
patch,
|
|
1033
|
+
error: `Search string not found in file. First 50 chars of search: "${normalizedSearch.substring(0, 50)}..."`,
|
|
1034
|
+
});
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// If regex matched, use regex replace
|
|
1039
|
+
content = content.replace(regex, normalizedReplace);
|
|
1040
|
+
appliedPatches++;
|
|
1041
|
+
} else {
|
|
1042
|
+
// Exact match found - apply the replacement
|
|
1043
|
+
// Only replace the first occurrence to be safe
|
|
1044
|
+
const index = content.indexOf(normalizedSearch);
|
|
1045
|
+
content =
|
|
1046
|
+
content.substring(0, index) +
|
|
1047
|
+
normalizedReplace +
|
|
1048
|
+
content.substring(index + normalizedSearch.length);
|
|
1049
|
+
appliedPatches++;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return {
|
|
1054
|
+
success: failedPatches.length === 0,
|
|
1055
|
+
modifiedContent: content,
|
|
1056
|
+
appliedPatches,
|
|
1057
|
+
failedPatches,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
|
|
704
1061
|
/**
|
|
705
1062
|
* Validate that AI modifications are surgical edits, not complete rewrites
|
|
706
1063
|
*/
|