@getcoherent/cli 0.5.3 → 0.5.5
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/index.js +1867 -1667
- package/package.json +10 -10
- package/LICENSE +0 -21
package/dist/index.js
CHANGED
|
@@ -3909,6 +3909,7 @@ DROPDOWN MENU:
|
|
|
3909
3909
|
- Item text: text-sm. Icon before text: size-4 mr-2.
|
|
3910
3910
|
- Group related items with DropdownMenuSeparator.
|
|
3911
3911
|
- Destructive item: className="text-destructive" at bottom, separated.
|
|
3912
|
+
- NON-DESTRUCTIVE items: NEVER apply text color classes. Use default text-foreground. No text-amber, text-orange, text-yellow on menu items.
|
|
3912
3913
|
- Keyboard shortcut hint: <DropdownMenuShortcut>\u2318K</DropdownMenuShortcut>.
|
|
3913
3914
|
- Max items without scroll: 8. For more, use Command palette.
|
|
3914
3915
|
- Trigger: Button variant="ghost" size="icon" for icon-only, variant="outline" for labeled.
|
|
@@ -4284,7 +4285,8 @@ async function parseModification(message, context, provider = "auto", options) {
|
|
|
4284
4285
|
const prompt2 = buildPlanOnlyPrompt(message, context.config);
|
|
4285
4286
|
const raw2 = await ai.parseModification(prompt2);
|
|
4286
4287
|
const requestsArray2 = Array.isArray(raw2) ? raw2 : raw2?.requests ?? [];
|
|
4287
|
-
|
|
4288
|
+
const navigation = !Array.isArray(raw2) && raw2?.navigation ? raw2.navigation : void 0;
|
|
4289
|
+
return { requests: requestsArray2, uxRecommendations: void 0, navigation };
|
|
4288
4290
|
}
|
|
4289
4291
|
const componentRegistry = buildComponentRegistry(context.componentManager);
|
|
4290
4292
|
let enhancedMessage = message;
|
|
@@ -4341,7 +4343,10 @@ Return ONLY a JSON object with this structure (no pageCode, no sections, no cont
|
|
|
4341
4343
|
{
|
|
4342
4344
|
"requests": [
|
|
4343
4345
|
{ "type": "add-page", "target": "new", "changes": { "id": "page-id", "name": "Page Name", "route": "/page-route" } }
|
|
4344
|
-
]
|
|
4346
|
+
],
|
|
4347
|
+
"navigation": {
|
|
4348
|
+
"type": "header"
|
|
4349
|
+
}
|
|
4345
4350
|
}
|
|
4346
4351
|
|
|
4347
4352
|
Rules:
|
|
@@ -4349,6 +4354,10 @@ Rules:
|
|
|
4349
4354
|
- Route must start with /
|
|
4350
4355
|
- Keep response under 500 tokens
|
|
4351
4356
|
- Do NOT include pageCode, sections, or any other fields
|
|
4357
|
+
- Navigation type: Detect from user's request and include in response:
|
|
4358
|
+
* "sidebar" \u2014 if user mentions sidebar, side menu, left panel, admin panel, or app has 6+ main sections
|
|
4359
|
+
* "header" \u2014 if user mentions top navigation, header nav, or app is simple (< 6 sections). This is the default.
|
|
4360
|
+
* "both" \u2014 if complex multi-level app needs both header and sidebar navigation
|
|
4352
4361
|
- Include ALL pages the user explicitly requested
|
|
4353
4362
|
- ALSO include logically related pages that a real app would need. For example:
|
|
4354
4363
|
* If there is a catalog/listing page, add a detail page (e.g. /products \u2192 /products/[id])
|
|
@@ -4487,6 +4496,7 @@ LAYOUT CONTRACT (CRITICAL \u2014 prevents duplicate navigation and footer):
|
|
|
4487
4496
|
- The app has a root layout (app/layout.tsx) that renders a shared Header and Footer.
|
|
4488
4497
|
- Pages are rendered INSIDE this layout, between the Header and Footer.
|
|
4489
4498
|
- NEVER include <header>, <nav>, or <footer> elements in pageCode. Also do NOT add a footer-like section at the bottom (no "\xA9 2024", no site links, no logo + nav links at the bottom).
|
|
4499
|
+
- NEVER generate a sidebar panel, navigation column, or left-side navigation inside pageCode. Sidebar navigation is handled by the layout system via shared Sidebar component. If the user mentions "sidebar", it will be rendered by app/(app)/layout.tsx \u2014 do NOT recreate it in page code.
|
|
4490
4500
|
- If the page needs sub-navigation (tabs, breadcrumbs, sidebar nav), use elements like <div role="tablist"> or <aside> \u2014 NOT <header>, <nav>, or <footer>.
|
|
4491
4501
|
- Do NOT add any navigation bars, logo headers, site-wide menus, or site footers to pages. The layout provides all of these.
|
|
4492
4502
|
|
|
@@ -4503,7 +4513,7 @@ PAGE CONTENT (CRITICAL \u2014 prevents empty or duplicate pages):
|
|
|
4503
4513
|
- Every page MUST have substantial content. NEVER generate a page with only metadata and an empty <main> element.
|
|
4504
4514
|
- NEVER create an inline preview/demo of another page (e.g., embedding a "dashboard view" inside the landing page with a toggle). Each page should be its own route.
|
|
4505
4515
|
- NEVER create a single-page app (SPA) that renders multiple views via useState. Each view must be a separate Next.js page with its own route.
|
|
4506
|
-
- The home page (route "/")
|
|
4516
|
+
- The home page (route "/"): When BOTH "/" and "/dashboard" exist, "/" MUST be a full landing page with hero section, features, pricing, and CTA buttons linking to /login and /register \u2014 NOT a redirect. When "/" is the ONLY main page, it can be a redirect to /dashboard. NEVER a multi-view SPA.
|
|
4507
4517
|
- Landing pages should link to app pages via <Link href="/dashboard">, NOT via useState toggles that render inline content.
|
|
4508
4518
|
|
|
4509
4519
|
pageCode rules (shadcn/ui blocks quality):
|
|
@@ -4515,8 +4525,10 @@ pageCode rules (shadcn/ui blocks quality):
|
|
|
4515
4525
|
- No placeholders: real contextual copy only. Use the EXACT text, language, and content from the user's request.
|
|
4516
4526
|
- IMAGES: For avatar/profile photos, use https://i.pravatar.cc/150?u=<unique-seed> (e.g. ?u=sarah.johnson). For hero/product images, use https://picsum.photos/800/400?random=N. Use standard <img> tags with className, NOT Next.js <Image>. Always provide alt text.
|
|
4517
4527
|
- BUTTON + LINK: The Button component supports asChild prop. To make a button that navigates, use <Button asChild><Link href="/path"><Plus className="size-4" /> Label</Link></Button>. Never nest <button> inside <Link> or vice versa without asChild.
|
|
4528
|
+
- DOM NESTING: NEVER nest interactive elements. No <Button> inside <Link>, no <a> inside <a>, no <button> inside <button>. For clickable cards containing internal buttons, use onClick on the card wrapper \u2014 NOT <Link> wrapping the entire card.
|
|
4518
4529
|
- Hover/focus on every interactive element (hover:bg-muted, focus-visible:ring-2 focus-visible:ring-ring).
|
|
4519
4530
|
- LANGUAGE: Match the language of the user's request. English request \u2192 English page. Russian request \u2192 Russian page. Never switch languages.
|
|
4531
|
+
- NEVER use native HTML <select> or <option>. Always use Select, SelectTrigger, SelectValue, SelectContent, SelectItem from @/components/ui/select.
|
|
4520
4532
|
|
|
4521
4533
|
NEXT.JS APP ROUTER RULE (CRITICAL \u2014 invalid code fails to compile):
|
|
4522
4534
|
- "use client" and export const metadata are FORBIDDEN in the same file.
|
|
@@ -4704,7 +4716,7 @@ var AUTH_LAYOUT = `export default function AuthLayout({
|
|
|
4704
4716
|
children: React.ReactNode
|
|
4705
4717
|
}) {
|
|
4706
4718
|
return (
|
|
4707
|
-
<div className="min-h-svh bg-muted">
|
|
4719
|
+
<div className="min-h-svh bg-muted flex items-center justify-center p-4">
|
|
4708
4720
|
{children}
|
|
4709
4721
|
</div>
|
|
4710
4722
|
)
|
|
@@ -5042,1716 +5054,1861 @@ function fixGlobalsCss(projectRoot, config2) {
|
|
|
5042
5054
|
writeFileSync7(layoutPath, layoutContent, "utf-8");
|
|
5043
5055
|
}
|
|
5044
5056
|
|
|
5045
|
-
// src/
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
var
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
if (
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
return pages.filter((page, idx) => {
|
|
5085
|
-
const norm = normalize(page.route);
|
|
5086
|
-
if (seen.has(norm)) return false;
|
|
5087
|
-
seen.set(norm, idx);
|
|
5088
|
-
return true;
|
|
5089
|
-
});
|
|
5090
|
-
}
|
|
5091
|
-
function extractComponentIdsFromCode(code) {
|
|
5092
|
-
const ids = /* @__PURE__ */ new Set();
|
|
5093
|
-
const allMatches = code.matchAll(/@\/components\/((?:ui\/)?[a-z0-9-]+)/g);
|
|
5094
|
-
for (const m of allMatches) {
|
|
5095
|
-
if (!m[1]) continue;
|
|
5096
|
-
let id = m[1];
|
|
5097
|
-
if (id.startsWith("ui/")) id = id.slice(3);
|
|
5098
|
-
if (id === "shared" || id.startsWith("shared/")) continue;
|
|
5099
|
-
if (id) ids.add(id);
|
|
5057
|
+
// src/utils/quality-validator.ts
|
|
5058
|
+
var RAW_COLOR_RE = /(?:bg|text|border)-(gray|blue|red|green|yellow|purple|pink|indigo|orange|slate|zinc|stone|neutral|emerald|teal|cyan|sky|violet|fuchsia|rose|amber|lime)-\d+/g;
|
|
5059
|
+
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
5060
|
+
var TEXT_BASE_RE = /\btext-base\b/g;
|
|
5061
|
+
var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
|
|
5062
|
+
var SM_BREAKPOINT_RE = /\bsm:/g;
|
|
5063
|
+
var XL_BREAKPOINT_RE = /\bxl:/g;
|
|
5064
|
+
var XXL_BREAKPOINT_RE = /\b2xl:/g;
|
|
5065
|
+
var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
|
|
5066
|
+
var RAW_BUTTON_RE = /<button\b/g;
|
|
5067
|
+
var RAW_INPUT_RE = /<input\b/g;
|
|
5068
|
+
var RAW_SELECT_RE = /<select\b/g;
|
|
5069
|
+
var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
|
|
5070
|
+
var NATIVE_TABLE_RE = /<table\b/g;
|
|
5071
|
+
var PLACEHOLDER_PATTERNS = [
|
|
5072
|
+
/>\s*Lorem ipsum\b/i,
|
|
5073
|
+
/>\s*Card content\s*</i,
|
|
5074
|
+
/>\s*Your (?:text|content) here\s*</i,
|
|
5075
|
+
/>\s*Description\s*</,
|
|
5076
|
+
/>\s*Title\s*</,
|
|
5077
|
+
/placeholder\s*text/i
|
|
5078
|
+
];
|
|
5079
|
+
var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
|
|
5080
|
+
var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
|
|
5081
|
+
var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
|
|
5082
|
+
var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
|
|
5083
|
+
function isInsideCommentOrString(line, matchIndex) {
|
|
5084
|
+
const commentIdx = line.indexOf("//");
|
|
5085
|
+
if (commentIdx !== -1 && commentIdx < matchIndex) return true;
|
|
5086
|
+
let inSingle = false;
|
|
5087
|
+
let inDouble = false;
|
|
5088
|
+
let inTemplate = false;
|
|
5089
|
+
for (let i = 0; i < matchIndex; i++) {
|
|
5090
|
+
const ch = line[i];
|
|
5091
|
+
const prev = i > 0 ? line[i - 1] : "";
|
|
5092
|
+
if (prev === "\\") continue;
|
|
5093
|
+
if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
|
|
5094
|
+
if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
|
|
5095
|
+
if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
|
|
5100
5096
|
}
|
|
5101
|
-
return
|
|
5097
|
+
return inSingle || inDouble || inTemplate;
|
|
5102
5098
|
}
|
|
5103
|
-
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
const
|
|
5109
|
-
if (
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
)
|
|
5117
|
-
);
|
|
5118
|
-
continue;
|
|
5119
|
-
}
|
|
5120
|
-
try {
|
|
5121
|
-
const fullPath = resolve5(projectRoot, e.file);
|
|
5122
|
-
const sharedSnippet = (await readFile(fullPath)).slice(0, 600);
|
|
5123
|
-
const sharedTokens = new Set(sharedSnippet.match(/\b[a-zA-Z0-9-]{4,}\b/g) ?? []);
|
|
5124
|
-
const pageTokens = pageCode.match(/\b[a-zA-Z0-9-]+\b/g) ?? [];
|
|
5125
|
-
let overlap = 0;
|
|
5126
|
-
for (const t of sharedTokens) {
|
|
5127
|
-
if (pageTokens.includes(t)) overlap++;
|
|
5099
|
+
function checkLines(code, pattern, type, message, severity, skipCommentsAndStrings = false) {
|
|
5100
|
+
const issues = [];
|
|
5101
|
+
const lines = code.split("\n");
|
|
5102
|
+
let inBlockComment = false;
|
|
5103
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5104
|
+
const line = lines[i];
|
|
5105
|
+
if (skipCommentsAndStrings) {
|
|
5106
|
+
if (inBlockComment) {
|
|
5107
|
+
const endIdx = line.indexOf("*/");
|
|
5108
|
+
if (endIdx !== -1) {
|
|
5109
|
+
inBlockComment = false;
|
|
5110
|
+
}
|
|
5111
|
+
continue;
|
|
5128
5112
|
}
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
5134
|
-
)
|
|
5135
|
-
);
|
|
5113
|
+
const blockStart = line.indexOf("/*");
|
|
5114
|
+
if (blockStart !== -1 && !line.includes("*/")) {
|
|
5115
|
+
inBlockComment = true;
|
|
5116
|
+
continue;
|
|
5136
5117
|
}
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
`Design system config not found at ${configPath}
|
|
5145
|
-
Run "coherent init" first to create a project.`
|
|
5146
|
-
);
|
|
5147
|
-
}
|
|
5148
|
-
const manager = new DesignSystemManager3(configPath);
|
|
5149
|
-
await manager.load();
|
|
5150
|
-
return manager.getConfig();
|
|
5151
|
-
}
|
|
5152
|
-
function requireProject() {
|
|
5153
|
-
const project = findConfig();
|
|
5154
|
-
if (!project) {
|
|
5155
|
-
exitNotCoherent();
|
|
5156
|
-
}
|
|
5157
|
-
warnIfVolatile(project.root);
|
|
5158
|
-
return project;
|
|
5159
|
-
}
|
|
5160
|
-
async function resolveTargetFlags(message, options, config2, projectRoot) {
|
|
5161
|
-
if (options.component) {
|
|
5162
|
-
const manifest = await loadManifest4(projectRoot);
|
|
5163
|
-
const target = options.component;
|
|
5164
|
-
const entry = manifest.shared.find(
|
|
5165
|
-
(s) => s.name.toLowerCase() === target.toLowerCase() || s.id.toLowerCase() === target.toLowerCase()
|
|
5166
|
-
);
|
|
5167
|
-
if (entry) {
|
|
5168
|
-
const filePath = resolve5(projectRoot, entry.file);
|
|
5169
|
-
let currentCode = "";
|
|
5170
|
-
if (existsSync13(filePath)) {
|
|
5171
|
-
currentCode = readFileSync8(filePath, "utf-8");
|
|
5118
|
+
let m;
|
|
5119
|
+
pattern.lastIndex = 0;
|
|
5120
|
+
while ((m = pattern.exec(line)) !== null) {
|
|
5121
|
+
if (!isInsideCommentOrString(line, m.index)) {
|
|
5122
|
+
issues.push({ line: i + 1, type, message, severity });
|
|
5123
|
+
break;
|
|
5124
|
+
}
|
|
5172
5125
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
${currentCode}
|
|
5178
|
-
\`\`\`` : "";
|
|
5179
|
-
return `Modify the shared component ${entry.name} (${entry.id}, file: ${entry.file}): ${message}. Read the current code below and apply the requested changes. Return the full updated component code as pageCode.${codeSnippet}`;
|
|
5180
|
-
}
|
|
5181
|
-
console.log(chalk8.yellow(`
|
|
5182
|
-
\u26A0\uFE0F Component "${target}" not found in shared components.`));
|
|
5183
|
-
console.log(chalk8.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
|
|
5184
|
-
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
5185
|
-
}
|
|
5186
|
-
if (options.page) {
|
|
5187
|
-
const target = options.page;
|
|
5188
|
-
const page = config2.pages.find(
|
|
5189
|
-
(p) => p.name.toLowerCase() === target.toLowerCase() || p.id.toLowerCase() === target.toLowerCase() || p.route === target || p.route === "/" + target
|
|
5190
|
-
);
|
|
5191
|
-
if (page) {
|
|
5192
|
-
const relPath = page.route === "/" ? "app/page.tsx" : `app${page.route}/page.tsx`;
|
|
5193
|
-
const filePath = resolve5(projectRoot, relPath);
|
|
5194
|
-
let currentCode = "";
|
|
5195
|
-
if (existsSync13(filePath)) {
|
|
5196
|
-
currentCode = readFileSync8(filePath, "utf-8");
|
|
5126
|
+
} else {
|
|
5127
|
+
pattern.lastIndex = 0;
|
|
5128
|
+
if (pattern.test(line)) {
|
|
5129
|
+
issues.push({ line: i + 1, type, message, severity });
|
|
5197
5130
|
}
|
|
5198
|
-
const codeSnippet = currentCode ? `
|
|
5199
|
-
|
|
5200
|
-
Current code of ${page.name} page:
|
|
5201
|
-
\`\`\`tsx
|
|
5202
|
-
${currentCode}
|
|
5203
|
-
\`\`\`` : "";
|
|
5204
|
-
return `Update page "${page.name}" (id: ${page.id}, route: ${page.route}, file: ${relPath}): ${message}. Read the current code below and apply the requested changes.${codeSnippet}`;
|
|
5205
5131
|
}
|
|
5206
|
-
console.log(chalk8.yellow(`
|
|
5207
|
-
\u26A0\uFE0F Page "${target}" not found.`));
|
|
5208
|
-
console.log(chalk8.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
|
|
5209
|
-
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
5210
|
-
}
|
|
5211
|
-
if (options.token) {
|
|
5212
|
-
const target = options.token;
|
|
5213
|
-
return `Change design token "${target}": ${message}. Update the token value in design-system.config.ts and ensure globals.css reflects the change.`;
|
|
5214
5132
|
}
|
|
5215
|
-
return
|
|
5133
|
+
return issues;
|
|
5216
5134
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
};
|
|
5227
|
-
var PAGE_RELATIONSHIP_RULES = [
|
|
5228
|
-
{
|
|
5229
|
-
trigger: /\/(products|catalog|marketplace|listings|shop|store)\b/i,
|
|
5230
|
-
related: [{ id: "product-detail", name: "Product Detail", route: "/products/[id]" }]
|
|
5231
|
-
},
|
|
5232
|
-
{
|
|
5233
|
-
trigger: /\/(blog|news|articles|posts)\b/i,
|
|
5234
|
-
related: [{ id: "article-detail", name: "Article", route: "/blog/[slug]" }]
|
|
5235
|
-
},
|
|
5236
|
-
{
|
|
5237
|
-
trigger: /\/(campaigns|ads|ad-campaigns)\b/i,
|
|
5238
|
-
related: [{ id: "campaign-detail", name: "Campaign Detail", route: "/campaigns/[id]" }]
|
|
5239
|
-
},
|
|
5240
|
-
{
|
|
5241
|
-
trigger: /\/(dashboard|admin)\b/i,
|
|
5242
|
-
related: [{ id: "settings", name: "Settings", route: "/settings" }]
|
|
5243
|
-
},
|
|
5244
|
-
{
|
|
5245
|
-
trigger: /\/pricing\b/i,
|
|
5246
|
-
related: [{ id: "checkout", name: "Checkout", route: "/checkout" }]
|
|
5247
|
-
}
|
|
5248
|
-
];
|
|
5249
|
-
function extractInternalLinks(code) {
|
|
5250
|
-
const links = /* @__PURE__ */ new Set();
|
|
5251
|
-
const hrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
|
|
5252
|
-
let m;
|
|
5253
|
-
while ((m = hrefRe.exec(code)) !== null) {
|
|
5254
|
-
const route = m[1];
|
|
5255
|
-
if (route === "/" || route.startsWith("/design-system") || route.startsWith("/#") || route.startsWith("/api"))
|
|
5256
|
-
continue;
|
|
5257
|
-
links.add(route);
|
|
5258
|
-
}
|
|
5259
|
-
return [...links];
|
|
5260
|
-
}
|
|
5261
|
-
function inferRelatedPages(plannedPages) {
|
|
5262
|
-
const plannedRoutes = new Set(plannedPages.map((p) => p.route));
|
|
5263
|
-
const inferred = [];
|
|
5264
|
-
for (const { route } of plannedPages) {
|
|
5265
|
-
const authRelated = AUTH_FLOW_PATTERNS[route];
|
|
5266
|
-
if (authRelated) {
|
|
5267
|
-
for (const rel of authRelated) {
|
|
5268
|
-
if (!plannedRoutes.has(rel)) {
|
|
5269
|
-
const slug = rel.slice(1);
|
|
5270
|
-
const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
5271
|
-
inferred.push({ id: slug, name, route: rel });
|
|
5272
|
-
plannedRoutes.add(rel);
|
|
5273
|
-
}
|
|
5274
|
-
}
|
|
5275
|
-
}
|
|
5276
|
-
for (const rule of PAGE_RELATIONSHIP_RULES) {
|
|
5277
|
-
if (rule.trigger.test(route)) {
|
|
5278
|
-
for (const rel of rule.related) {
|
|
5279
|
-
if (!plannedRoutes.has(rel.route)) {
|
|
5280
|
-
inferred.push(rel);
|
|
5281
|
-
plannedRoutes.add(rel.route);
|
|
5282
|
-
}
|
|
5283
|
-
}
|
|
5284
|
-
}
|
|
5285
|
-
}
|
|
5286
|
-
}
|
|
5287
|
-
return inferred;
|
|
5288
|
-
}
|
|
5289
|
-
function impliesFullWebsite(message) {
|
|
5290
|
-
return /\b(create|build|make|design)\b.{0,80}\b(website|web\s*site|web\s*app|application|app|platform|portal|marketplace|site)\b/i.test(
|
|
5291
|
-
message
|
|
5292
|
-
);
|
|
5293
|
-
}
|
|
5294
|
-
function extractPageNamesFromMessage(message) {
|
|
5295
|
-
const pages = [];
|
|
5296
|
-
const known = {
|
|
5297
|
-
home: "/",
|
|
5298
|
-
landing: "/",
|
|
5299
|
-
dashboard: "/dashboard",
|
|
5300
|
-
about: "/about",
|
|
5301
|
-
"about us": "/about",
|
|
5302
|
-
contact: "/contact",
|
|
5303
|
-
contacts: "/contacts",
|
|
5304
|
-
pricing: "/pricing",
|
|
5305
|
-
settings: "/settings",
|
|
5306
|
-
account: "/account",
|
|
5307
|
-
"personal account": "/account",
|
|
5308
|
-
registration: "/registration",
|
|
5309
|
-
signup: "/signup",
|
|
5310
|
-
"sign up": "/signup",
|
|
5311
|
-
login: "/login",
|
|
5312
|
-
"sign in": "/login",
|
|
5313
|
-
catalogue: "/catalogue",
|
|
5314
|
-
catalog: "/catalog",
|
|
5315
|
-
blog: "/blog",
|
|
5316
|
-
portfolio: "/portfolio",
|
|
5317
|
-
features: "/features",
|
|
5318
|
-
services: "/services",
|
|
5319
|
-
faq: "/faq",
|
|
5320
|
-
team: "/team"
|
|
5321
|
-
};
|
|
5322
|
-
const lower = message.toLowerCase();
|
|
5323
|
-
for (const [key, route] of Object.entries(known)) {
|
|
5324
|
-
if (lower.includes(key)) {
|
|
5325
|
-
const name = key.split(" ").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
5326
|
-
const id = route.slice(1) || "home";
|
|
5327
|
-
if (!pages.some((p) => p.route === route)) {
|
|
5328
|
-
pages.push({ name, id, route });
|
|
5329
|
-
}
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5332
|
-
return pages;
|
|
5333
|
-
}
|
|
5334
|
-
function normalizeRequest(request, config2) {
|
|
5335
|
-
const changes = request.changes;
|
|
5336
|
-
const VALID_TYPES = [
|
|
5337
|
-
"update-token",
|
|
5338
|
-
"add-component",
|
|
5339
|
-
"modify-component",
|
|
5340
|
-
"add-layout-block",
|
|
5341
|
-
"modify-layout-block",
|
|
5342
|
-
"add-page",
|
|
5343
|
-
"update-page",
|
|
5344
|
-
"update-navigation",
|
|
5345
|
-
"link-shared",
|
|
5346
|
-
"promote-and-link"
|
|
5347
|
-
];
|
|
5348
|
-
if (!VALID_TYPES.includes(request.type)) {
|
|
5349
|
-
return { error: `Unknown action "${request.type}". Valid: ${VALID_TYPES.join(", ")}` };
|
|
5350
|
-
}
|
|
5351
|
-
const findPage = (target) => config2.pages.find(
|
|
5352
|
-
(p) => p.id === target || p.route === target || p.name?.toLowerCase() === String(target).toLowerCase()
|
|
5353
|
-
);
|
|
5354
|
-
switch (request.type) {
|
|
5355
|
-
case "update-page": {
|
|
5356
|
-
const page = findPage(request.target);
|
|
5357
|
-
if (!page && changes?.pageCode) {
|
|
5358
|
-
const targetStr = String(request.target);
|
|
5359
|
-
const id = targetStr.replace(/^\//, "") || "home";
|
|
5360
|
-
return {
|
|
5361
|
-
...request,
|
|
5362
|
-
type: "add-page",
|
|
5363
|
-
target: "new",
|
|
5364
|
-
changes: {
|
|
5365
|
-
id,
|
|
5366
|
-
name: changes.name || id.charAt(0).toUpperCase() + id.slice(1) || "Home",
|
|
5367
|
-
route: targetStr.startsWith("/") ? targetStr : `/${targetStr}`,
|
|
5368
|
-
...changes
|
|
5369
|
-
}
|
|
5370
|
-
};
|
|
5371
|
-
}
|
|
5372
|
-
if (!page) {
|
|
5373
|
-
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
5374
|
-
return { error: `Page "${request.target}" not found. Available: ${available || "none"}` };
|
|
5375
|
-
}
|
|
5376
|
-
if (page.id !== request.target) {
|
|
5377
|
-
return { ...request, target: page.id };
|
|
5378
|
-
}
|
|
5379
|
-
break;
|
|
5380
|
-
}
|
|
5381
|
-
case "add-page": {
|
|
5382
|
-
if (!changes) break;
|
|
5383
|
-
let route = changes.route || "";
|
|
5384
|
-
if (route && !route.startsWith("/")) route = `/${route}`;
|
|
5385
|
-
if (route) changes.route = route;
|
|
5386
|
-
const existingByRoute = config2.pages.find((p) => p.route === route);
|
|
5387
|
-
if (existingByRoute && route) {
|
|
5388
|
-
return {
|
|
5389
|
-
...request,
|
|
5390
|
-
type: "update-page",
|
|
5391
|
-
target: existingByRoute.id
|
|
5392
|
-
};
|
|
5393
|
-
}
|
|
5394
|
-
if (!changes.id && changes.name) {
|
|
5395
|
-
changes.id = String(changes.name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
5396
|
-
}
|
|
5397
|
-
if (!changes.id && route) {
|
|
5398
|
-
changes.id = route.replace(/^\//, "") || "home";
|
|
5399
|
-
}
|
|
5400
|
-
break;
|
|
5401
|
-
}
|
|
5402
|
-
case "modify-component": {
|
|
5403
|
-
const componentId = request.target;
|
|
5404
|
-
const existingComp = config2.components.find((c) => c.id === componentId);
|
|
5405
|
-
if (!existingComp) {
|
|
5406
|
-
return {
|
|
5407
|
-
...request,
|
|
5408
|
-
type: "add-component",
|
|
5409
|
-
target: "new"
|
|
5410
|
-
};
|
|
5411
|
-
}
|
|
5412
|
-
if (changes) {
|
|
5413
|
-
if (typeof changes.id === "string" && changes.id !== componentId) {
|
|
5414
|
-
const targetExists = config2.components.some((c) => c.id === changes.id);
|
|
5415
|
-
if (!targetExists) {
|
|
5416
|
-
return { ...request, type: "add-component", target: "new" };
|
|
5417
|
-
}
|
|
5418
|
-
return {
|
|
5419
|
-
error: `Cannot change component "${componentId}" to "${changes.id}" \u2014 "${changes.id}" already exists.`
|
|
5420
|
-
};
|
|
5421
|
-
}
|
|
5422
|
-
if (typeof changes.name === "string") {
|
|
5423
|
-
const newName = changes.name.toLowerCase();
|
|
5424
|
-
const curName = existingComp.name.toLowerCase();
|
|
5425
|
-
const curId = componentId.toLowerCase();
|
|
5426
|
-
const nameOk = newName === curName || newName === curId || newName.includes(curId) || curId.includes(newName);
|
|
5427
|
-
if (!nameOk) {
|
|
5428
|
-
delete changes.name;
|
|
5429
|
-
}
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5432
|
-
break;
|
|
5433
|
-
}
|
|
5434
|
-
case "add-component": {
|
|
5435
|
-
if (changes) {
|
|
5436
|
-
const shadcn = changes.shadcnComponent;
|
|
5437
|
-
const id = changes.id;
|
|
5438
|
-
if (shadcn && id && id !== shadcn) {
|
|
5439
|
-
changes.id = shadcn;
|
|
5440
|
-
}
|
|
5441
|
-
}
|
|
5442
|
-
break;
|
|
5443
|
-
}
|
|
5444
|
-
case "link-shared": {
|
|
5445
|
-
if (changes) {
|
|
5446
|
-
const page = findPage(request.target);
|
|
5447
|
-
if (!page) {
|
|
5448
|
-
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
5449
|
-
return { error: `Page "${request.target}" not found for link-shared. Available: ${available || "none"}` };
|
|
5450
|
-
}
|
|
5451
|
-
if (page.id !== request.target) {
|
|
5452
|
-
return { ...request, target: page.id };
|
|
5453
|
-
}
|
|
5454
|
-
}
|
|
5455
|
-
break;
|
|
5456
|
-
}
|
|
5457
|
-
case "promote-and-link": {
|
|
5458
|
-
const sourcePage = findPage(request.target);
|
|
5459
|
-
if (!sourcePage) {
|
|
5460
|
-
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
5461
|
-
return {
|
|
5462
|
-
error: `Source page "${request.target}" not found for promote-and-link. Available: ${available || "none"}`
|
|
5463
|
-
};
|
|
5464
|
-
}
|
|
5465
|
-
if (sourcePage.id !== request.target) {
|
|
5466
|
-
return { ...request, target: sourcePage.id };
|
|
5467
|
-
}
|
|
5468
|
-
break;
|
|
5469
|
-
}
|
|
5470
|
-
}
|
|
5471
|
-
return request;
|
|
5472
|
-
}
|
|
5473
|
-
function applyDefaults(request) {
|
|
5474
|
-
if (request.type === "add-page" && request.changes && typeof request.changes === "object") {
|
|
5475
|
-
const changes = request.changes;
|
|
5476
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5477
|
-
const name = changes.name || "New Page";
|
|
5478
|
-
let id = changes.id || name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
5479
|
-
if (!/^[a-z]/.test(id)) id = `page-${id}`;
|
|
5480
|
-
const route = changes.route || `/${id}`;
|
|
5481
|
-
const hasPageCode = typeof changes.pageCode === "string" && changes.pageCode.trim() !== "";
|
|
5482
|
-
const base = {
|
|
5483
|
-
id,
|
|
5484
|
-
name,
|
|
5485
|
-
route: route.startsWith("/") ? route : `/${route}`,
|
|
5486
|
-
layout: changes.layout || "centered",
|
|
5487
|
-
title: changes.title || name,
|
|
5488
|
-
description: changes.description || `${name} page`,
|
|
5489
|
-
createdAt: changes.createdAt || now,
|
|
5490
|
-
updatedAt: changes.updatedAt || now,
|
|
5491
|
-
requiresAuth: changes.requiresAuth ?? false,
|
|
5492
|
-
noIndex: changes.noIndex ?? false
|
|
5493
|
-
};
|
|
5494
|
-
const sections = Array.isArray(changes.sections) ? changes.sections.map((section, idx) => ({
|
|
5495
|
-
id: section.id || `section-${idx}`,
|
|
5496
|
-
name: section.name || `Section ${idx + 1}`,
|
|
5497
|
-
componentId: section.componentId || "button",
|
|
5498
|
-
order: typeof section.order === "number" ? section.order : idx,
|
|
5499
|
-
props: section.props || {}
|
|
5500
|
-
})) : [];
|
|
5501
|
-
return {
|
|
5502
|
-
...request,
|
|
5503
|
-
changes: {
|
|
5504
|
-
...base,
|
|
5505
|
-
sections,
|
|
5506
|
-
...hasPageCode ? { pageCode: changes.pageCode, generatedWithPageCode: true } : {},
|
|
5507
|
-
...changes.pageType ? { pageType: changes.pageType } : {},
|
|
5508
|
-
...changes.structuredContent ? { structuredContent: changes.structuredContent } : {}
|
|
5509
|
-
}
|
|
5510
|
-
};
|
|
5511
|
-
}
|
|
5512
|
-
if (request.type === "add-component" && request.changes && typeof request.changes === "object") {
|
|
5513
|
-
const changes = request.changes;
|
|
5514
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5515
|
-
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
5516
|
-
let normalizedVariants = [];
|
|
5517
|
-
if (Array.isArray(changes.variants)) {
|
|
5518
|
-
normalizedVariants = changes.variants.map((v) => {
|
|
5519
|
-
if (typeof v === "string") return { name: v, className: "" };
|
|
5520
|
-
if (v && typeof v === "object" && "name" in v) {
|
|
5521
|
-
return {
|
|
5522
|
-
name: v.name,
|
|
5523
|
-
className: v.className ?? ""
|
|
5524
|
-
};
|
|
5525
|
-
}
|
|
5526
|
-
return { name: "default", className: "" };
|
|
5527
|
-
});
|
|
5528
|
-
}
|
|
5529
|
-
let normalizedSizes = [];
|
|
5530
|
-
if (Array.isArray(changes.sizes)) {
|
|
5531
|
-
normalizedSizes = changes.sizes.map((s) => {
|
|
5532
|
-
if (typeof s === "string") {
|
|
5533
|
-
const name = validSizeNames.includes(s) ? s : "md";
|
|
5534
|
-
return { name, className: "" };
|
|
5535
|
-
}
|
|
5536
|
-
if (s && typeof s === "object" && "name" in s) {
|
|
5537
|
-
const raw = s.name;
|
|
5538
|
-
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
5539
|
-
return { name, className: s.className ?? "" };
|
|
5540
|
-
}
|
|
5541
|
-
return { name: "md", className: "" };
|
|
5542
|
-
});
|
|
5543
|
-
}
|
|
5544
|
-
return {
|
|
5545
|
-
...request,
|
|
5546
|
-
changes: {
|
|
5547
|
-
...changes,
|
|
5548
|
-
variants: normalizedVariants,
|
|
5549
|
-
sizes: normalizedSizes,
|
|
5550
|
-
createdAt: now,
|
|
5551
|
-
updatedAt: now
|
|
5552
|
-
}
|
|
5553
|
-
};
|
|
5554
|
-
}
|
|
5555
|
-
if (request.type === "modify-component" && request.changes && typeof request.changes === "object") {
|
|
5556
|
-
const changes = request.changes;
|
|
5557
|
-
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
5558
|
-
let normalizedVariants;
|
|
5559
|
-
if (Array.isArray(changes.variants)) {
|
|
5560
|
-
normalizedVariants = changes.variants.map((v) => {
|
|
5561
|
-
if (typeof v === "string") return { name: v, className: "" };
|
|
5562
|
-
if (v && typeof v === "object" && "name" in v) {
|
|
5563
|
-
return {
|
|
5564
|
-
name: v.name,
|
|
5565
|
-
className: v.className ?? ""
|
|
5566
|
-
};
|
|
5567
|
-
}
|
|
5568
|
-
return { name: "default", className: "" };
|
|
5569
|
-
});
|
|
5570
|
-
}
|
|
5571
|
-
let normalizedSizes;
|
|
5572
|
-
if (Array.isArray(changes.sizes)) {
|
|
5573
|
-
normalizedSizes = changes.sizes.map((s) => {
|
|
5574
|
-
if (typeof s === "string") {
|
|
5575
|
-
const name = validSizeNames.includes(s) ? s : "md";
|
|
5576
|
-
return { name, className: "" };
|
|
5577
|
-
}
|
|
5578
|
-
if (s && typeof s === "object" && "name" in s) {
|
|
5579
|
-
const raw = s.name;
|
|
5580
|
-
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
5581
|
-
return { name, className: s.className ?? "" };
|
|
5582
|
-
}
|
|
5583
|
-
return { name: "md", className: "" };
|
|
5584
|
-
});
|
|
5585
|
-
}
|
|
5586
|
-
return {
|
|
5587
|
-
...request,
|
|
5588
|
-
changes: {
|
|
5589
|
-
...changes,
|
|
5590
|
-
...normalizedVariants !== void 0 && { variants: normalizedVariants },
|
|
5591
|
-
...normalizedSizes !== void 0 && { sizes: normalizedSizes }
|
|
5592
|
-
}
|
|
5593
|
-
};
|
|
5594
|
-
}
|
|
5595
|
-
return request;
|
|
5596
|
-
}
|
|
5597
|
-
|
|
5598
|
-
// src/utils/page-analyzer.ts
|
|
5599
|
-
var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
|
|
5600
|
-
var VISUAL_WORDS = /\b(grid lines?|glow|radial|gradient|blur|shadow|overlay|animation|particles?|dots?|vertical|horizontal|decorat|behind|background|divider|spacer|wrapper|container|inner|outer|absolute|relative|translate|opacity|z-index|transition)\b/i;
|
|
5601
|
-
function analyzePageCode(code) {
|
|
5602
|
-
return {
|
|
5603
|
-
sections: extractSections(code),
|
|
5604
|
-
componentUsage: extractComponentUsage(code),
|
|
5605
|
-
iconCount: extractIconCount(code),
|
|
5606
|
-
layoutPattern: inferLayoutPattern(code),
|
|
5607
|
-
hasForm: detectFormUsage(code),
|
|
5608
|
-
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5135
|
+
function validatePageQuality(code, validRoutes) {
|
|
5136
|
+
const issues = [];
|
|
5137
|
+
const allLines = code.split("\n");
|
|
5138
|
+
const isTerminalContext = (lineNum) => {
|
|
5139
|
+
const start = Math.max(0, lineNum - 20);
|
|
5140
|
+
const nearby = allLines.slice(start, lineNum).join(" ");
|
|
5141
|
+
if (/font-mono/.test(allLines[lineNum - 1] || "")) return true;
|
|
5142
|
+
if (/bg-zinc-950|bg-zinc-900/.test(nearby) && /font-mono/.test(nearby)) return true;
|
|
5143
|
+
return false;
|
|
5609
5144
|
};
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5145
|
+
issues.push(
|
|
5146
|
+
...checkLines(
|
|
5147
|
+
code,
|
|
5148
|
+
RAW_COLOR_RE,
|
|
5149
|
+
"RAW_COLOR",
|
|
5150
|
+
"Raw Tailwind color detected \u2014 use semantic tokens (bg-primary, text-muted-foreground, etc.)",
|
|
5151
|
+
"error"
|
|
5152
|
+
).filter((issue) => !isTerminalContext(issue.line))
|
|
5153
|
+
);
|
|
5154
|
+
issues.push(
|
|
5155
|
+
...checkLines(
|
|
5156
|
+
code,
|
|
5157
|
+
HEX_IN_CLASS_RE,
|
|
5158
|
+
"HEX_IN_CLASS",
|
|
5159
|
+
"Hex color in className \u2014 use CSS variables via semantic tokens",
|
|
5160
|
+
"error"
|
|
5161
|
+
)
|
|
5162
|
+
);
|
|
5163
|
+
issues.push(
|
|
5164
|
+
...checkLines(code, TEXT_BASE_RE, "TEXT_BASE", "text-base detected \u2014 use text-sm as base font size", "warning")
|
|
5165
|
+
);
|
|
5166
|
+
issues.push(
|
|
5167
|
+
...checkLines(code, HEAVY_SHADOW_RE, "HEAVY_SHADOW", "Heavy shadow detected \u2014 use shadow-sm or none", "warning")
|
|
5168
|
+
);
|
|
5169
|
+
issues.push(
|
|
5170
|
+
...checkLines(
|
|
5171
|
+
code,
|
|
5172
|
+
SM_BREAKPOINT_RE,
|
|
5173
|
+
"SM_BREAKPOINT",
|
|
5174
|
+
"sm: breakpoint \u2014 consider if md:/lg: is sufficient",
|
|
5175
|
+
"info"
|
|
5176
|
+
)
|
|
5177
|
+
);
|
|
5178
|
+
issues.push(
|
|
5179
|
+
...checkLines(
|
|
5180
|
+
code,
|
|
5181
|
+
XL_BREAKPOINT_RE,
|
|
5182
|
+
"XL_BREAKPOINT",
|
|
5183
|
+
"xl: breakpoint \u2014 consider if md:/lg: is sufficient",
|
|
5184
|
+
"info"
|
|
5185
|
+
)
|
|
5186
|
+
);
|
|
5187
|
+
issues.push(
|
|
5188
|
+
...checkLines(
|
|
5189
|
+
code,
|
|
5190
|
+
XXL_BREAKPOINT_RE,
|
|
5191
|
+
"XXL_BREAKPOINT",
|
|
5192
|
+
"2xl: breakpoint \u2014 rarely needed, consider xl: instead",
|
|
5193
|
+
"warning"
|
|
5194
|
+
)
|
|
5195
|
+
);
|
|
5196
|
+
issues.push(
|
|
5197
|
+
...checkLines(
|
|
5198
|
+
code,
|
|
5199
|
+
LARGE_CARD_TITLE_RE,
|
|
5200
|
+
"LARGE_CARD_TITLE",
|
|
5201
|
+
"Large text on CardTitle \u2014 use text-sm font-medium",
|
|
5202
|
+
"warning"
|
|
5203
|
+
)
|
|
5204
|
+
);
|
|
5205
|
+
const codeLines = code.split("\n");
|
|
5206
|
+
issues.push(
|
|
5207
|
+
...checkLines(
|
|
5208
|
+
code,
|
|
5209
|
+
RAW_BUTTON_RE,
|
|
5210
|
+
"NATIVE_BUTTON",
|
|
5211
|
+
"Native <button> \u2014 use Button from @/components/ui/button",
|
|
5212
|
+
"error",
|
|
5213
|
+
true
|
|
5214
|
+
).filter((issue) => {
|
|
5215
|
+
const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
|
|
5216
|
+
if (nearby.includes("aria-label")) return false;
|
|
5217
|
+
if (/onClick=\{.*copy/i.test(nearby)) return false;
|
|
5218
|
+
return true;
|
|
5219
|
+
})
|
|
5220
|
+
);
|
|
5221
|
+
issues.push(
|
|
5222
|
+
...checkLines(
|
|
5223
|
+
code,
|
|
5224
|
+
RAW_SELECT_RE,
|
|
5225
|
+
"NATIVE_SELECT",
|
|
5226
|
+
"Native <select> \u2014 use Select from @/components/ui/select",
|
|
5227
|
+
"error",
|
|
5228
|
+
true
|
|
5229
|
+
)
|
|
5230
|
+
);
|
|
5231
|
+
issues.push(
|
|
5232
|
+
...checkLines(
|
|
5233
|
+
code,
|
|
5234
|
+
NATIVE_CHECKBOX_RE,
|
|
5235
|
+
"NATIVE_CHECKBOX",
|
|
5236
|
+
'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
|
|
5237
|
+
"error",
|
|
5238
|
+
true
|
|
5239
|
+
)
|
|
5240
|
+
);
|
|
5241
|
+
issues.push(
|
|
5242
|
+
...checkLines(
|
|
5243
|
+
code,
|
|
5244
|
+
NATIVE_TABLE_RE,
|
|
5245
|
+
"NATIVE_TABLE",
|
|
5246
|
+
"Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
|
|
5247
|
+
"warning",
|
|
5248
|
+
true
|
|
5249
|
+
)
|
|
5250
|
+
);
|
|
5251
|
+
const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5252
|
+
if (!hasInputImport) {
|
|
5253
|
+
issues.push(
|
|
5254
|
+
...checkLines(
|
|
5255
|
+
code,
|
|
5256
|
+
RAW_INPUT_RE,
|
|
5257
|
+
"RAW_INPUT",
|
|
5258
|
+
"Raw <input> element \u2014 import and use Input from @/components/ui/input",
|
|
5259
|
+
"warning",
|
|
5260
|
+
true
|
|
5261
|
+
)
|
|
5262
|
+
);
|
|
5628
5263
|
}
|
|
5629
|
-
|
|
5630
|
-
const
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5264
|
+
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
5265
|
+
const lines = code.split("\n");
|
|
5266
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5267
|
+
if (pattern.test(lines[i])) {
|
|
5268
|
+
issues.push({
|
|
5269
|
+
line: i + 1,
|
|
5270
|
+
type: "PLACEHOLDER",
|
|
5271
|
+
message: "Placeholder content detected \u2014 use real contextual content",
|
|
5272
|
+
severity: "error"
|
|
5273
|
+
});
|
|
5636
5274
|
}
|
|
5637
5275
|
}
|
|
5638
5276
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
importedComponents.push(...names);
|
|
5649
|
-
}
|
|
5650
|
-
for (const comp of importedComponents) {
|
|
5651
|
-
const re = new RegExp(`<${comp}[\\s/>]`, "g");
|
|
5652
|
-
const matches = code.match(re);
|
|
5653
|
-
usage[comp] = matches ? matches.length : 0;
|
|
5654
|
-
}
|
|
5655
|
-
return usage;
|
|
5656
|
-
}
|
|
5657
|
-
function extractIconCount(code) {
|
|
5658
|
-
const m = code.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
|
|
5659
|
-
if (!m) return 0;
|
|
5660
|
-
return m[1].split(",").map((s) => s.trim()).filter(Boolean).length;
|
|
5661
|
-
}
|
|
5662
|
-
function inferLayoutPattern(code) {
|
|
5663
|
-
const funcBodyMatch = code.match(/return\s*\(\s*(<[^]*)/s);
|
|
5664
|
-
const topLevel = funcBodyMatch ? funcBodyMatch[1].slice(0, 500) : code.slice(0, 800);
|
|
5665
|
-
if (/grid-cols|grid\s+md:grid-cols|grid\s+lg:grid-cols/.test(topLevel)) return "grid";
|
|
5666
|
-
if (/sidebar|aside/.test(topLevel)) return "sidebar";
|
|
5667
|
-
if (/max-w-\d|mx-auto|container/.test(topLevel)) return "centered";
|
|
5668
|
-
if (/min-h-screen|min-h-svh/.test(topLevel)) return "full-width";
|
|
5669
|
-
return "unknown";
|
|
5670
|
-
}
|
|
5671
|
-
function detectFormUsage(code) {
|
|
5672
|
-
const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
|
|
5673
|
-
let m;
|
|
5674
|
-
while ((m = importRe.exec(code)) !== null) {
|
|
5675
|
-
const names = m[1].split(",").map((s) => s.trim());
|
|
5676
|
-
if (names.some((n) => FORM_COMPONENTS.has(n))) return true;
|
|
5277
|
+
const hasGrid = /\bgrid\b/.test(code);
|
|
5278
|
+
const hasResponsive = /\bmd:|lg:/.test(code);
|
|
5279
|
+
if (hasGrid && !hasResponsive) {
|
|
5280
|
+
issues.push({
|
|
5281
|
+
line: 0,
|
|
5282
|
+
type: "NO_RESPONSIVE",
|
|
5283
|
+
message: "Grid layout without responsive breakpoints (md: or lg:)",
|
|
5284
|
+
severity: "warning"
|
|
5285
|
+
});
|
|
5677
5286
|
}
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5287
|
+
issues.push(
|
|
5288
|
+
...checkLines(
|
|
5289
|
+
code,
|
|
5290
|
+
IMG_WITHOUT_ALT_RE,
|
|
5291
|
+
"MISSING_ALT",
|
|
5292
|
+
'<img> without alt attribute \u2014 add descriptive alt or alt="" for decorative images',
|
|
5293
|
+
"error"
|
|
5294
|
+
)
|
|
5295
|
+
);
|
|
5296
|
+
issues.push(
|
|
5297
|
+
...checkLines(
|
|
5298
|
+
code,
|
|
5299
|
+
GENERIC_BUTTON_LABELS,
|
|
5300
|
+
"GENERIC_BUTTON_TEXT",
|
|
5301
|
+
'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
|
|
5302
|
+
"warning"
|
|
5303
|
+
)
|
|
5304
|
+
);
|
|
5305
|
+
const h1Matches = code.match(/<h1[\s>]/g);
|
|
5306
|
+
if (!h1Matches || h1Matches.length === 0) {
|
|
5307
|
+
issues.push({
|
|
5308
|
+
line: 0,
|
|
5309
|
+
type: "NO_H1",
|
|
5310
|
+
message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
|
|
5311
|
+
severity: "warning"
|
|
5312
|
+
});
|
|
5313
|
+
} else if (h1Matches.length > 1) {
|
|
5314
|
+
issues.push({
|
|
5315
|
+
line: 0,
|
|
5316
|
+
type: "MULTIPLE_H1",
|
|
5317
|
+
message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
|
|
5318
|
+
severity: "warning"
|
|
5319
|
+
});
|
|
5684
5320
|
}
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5321
|
+
const headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
|
|
5322
|
+
const hasCardContext = /\bCard\b|\bCardTitle\b|\bCardHeader\b/.test(code);
|
|
5323
|
+
for (let i = 1; i < headingLevels.length; i++) {
|
|
5324
|
+
if (headingLevels[i] > headingLevels[i - 1] + 1) {
|
|
5325
|
+
issues.push({
|
|
5326
|
+
line: 0,
|
|
5327
|
+
type: "SKIPPED_HEADING",
|
|
5328
|
+
message: `Heading level skipped: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]} \u2014 don't skip levels`,
|
|
5329
|
+
severity: hasCardContext ? "info" : "warning"
|
|
5330
|
+
});
|
|
5331
|
+
break;
|
|
5689
5332
|
}
|
|
5690
5333
|
}
|
|
5691
|
-
|
|
5692
|
-
|
|
5334
|
+
const hasLabelImport = /import\s.*Label.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5335
|
+
const inputCount = (code.match(INPUT_TAG_RE) || []).length;
|
|
5336
|
+
const labelForCount = (code.match(LABEL_FOR_RE) || []).length;
|
|
5337
|
+
if (hasLabelImport && inputCount > 0 && labelForCount === 0) {
|
|
5338
|
+
issues.push({
|
|
5339
|
+
line: 0,
|
|
5340
|
+
type: "MISSING_LABEL",
|
|
5341
|
+
message: "Inputs found but no Label with htmlFor \u2014 every input must have a visible label",
|
|
5342
|
+
severity: "error"
|
|
5343
|
+
});
|
|
5693
5344
|
}
|
|
5694
|
-
if (
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
let nextIndex = 0;
|
|
5702
|
-
async function worker() {
|
|
5703
|
-
while (nextIndex < items.length) {
|
|
5704
|
-
const i = nextIndex++;
|
|
5705
|
-
results[i] = await fn(items[i], i);
|
|
5706
|
-
}
|
|
5345
|
+
if (!hasLabelImport && inputCount > 0 && !/<label\b/i.test(code)) {
|
|
5346
|
+
issues.push({
|
|
5347
|
+
line: 0,
|
|
5348
|
+
type: "MISSING_LABEL",
|
|
5349
|
+
message: "Inputs found but no Label component \u2014 import Label and add htmlFor on each input",
|
|
5350
|
+
severity: "error"
|
|
5351
|
+
});
|
|
5707
5352
|
}
|
|
5708
|
-
const
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
const analyzed = pages.filter((p) => p.pageAnalysis);
|
|
5717
|
-
if (analyzed.length === 0) return "";
|
|
5718
|
-
const lines = analyzed.map((p) => {
|
|
5719
|
-
return summarizePageAnalysis(p.name || p.id, p.route, p.pageAnalysis);
|
|
5720
|
-
});
|
|
5721
|
-
let ctx = `EXISTING PAGES CONTEXT:
|
|
5722
|
-
${lines.join("\n")}
|
|
5723
|
-
|
|
5724
|
-
Use consistent component choices, spacing, and layout patterns across all pages. Match the style and structure of existing pages.`;
|
|
5725
|
-
const sp = config2.stylePatterns;
|
|
5726
|
-
if (sp && typeof sp === "object") {
|
|
5727
|
-
const parts = [];
|
|
5728
|
-
if (sp.card) parts.push(`Cards: ${sp.card}`);
|
|
5729
|
-
if (sp.section) parts.push(`Sections: ${sp.section}`);
|
|
5730
|
-
if (sp.terminal) parts.push(`Terminal blocks: ${sp.terminal}`);
|
|
5731
|
-
if (sp.iconContainer) parts.push(`Icon containers: ${sp.iconContainer}`);
|
|
5732
|
-
if (sp.heroHeadline) parts.push(`Hero headline: ${sp.heroHeadline}`);
|
|
5733
|
-
if (sp.sectionTitle) parts.push(`Section title: ${sp.sectionTitle}`);
|
|
5734
|
-
if (parts.length > 0) {
|
|
5735
|
-
ctx += `
|
|
5736
|
-
|
|
5737
|
-
PROJECT STYLE PATTERNS (from sync \u2014 match these exactly):
|
|
5738
|
-
${parts.join("\n")}`;
|
|
5739
|
-
}
|
|
5353
|
+
const hasPlaceholder = /placeholder\s*=/.test(code);
|
|
5354
|
+
if (hasPlaceholder && inputCount > 0 && labelForCount === 0 && !/<label\b/i.test(code) && !/<Label\b/.test(code)) {
|
|
5355
|
+
issues.push({
|
|
5356
|
+
line: 0,
|
|
5357
|
+
type: "PLACEHOLDER_ONLY_LABEL",
|
|
5358
|
+
message: "Inputs use placeholder only \u2014 add visible Label with htmlFor (placeholder is not a substitute)",
|
|
5359
|
+
severity: "error"
|
|
5360
|
+
});
|
|
5740
5361
|
}
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
)
|
|
5756
|
-
);
|
|
5757
|
-
const iconPatterns = unique(pageCode.match(/(?:rounded-\S+\s+)?p-\d+(?:\.\d+)?\s*(?:bg-\S+)?/g) || []).filter(
|
|
5758
|
-
(p) => p.includes("bg-") || p.includes("rounded")
|
|
5759
|
-
);
|
|
5760
|
-
const buttonPatterns = unique(
|
|
5761
|
-
(pageCode.match(/className="[^"]*(?:hover:|active:)[^"]*"/g) || []).map((m) => m.replace(/className="|"/g, "")).filter((c) => c.includes("px-") || c.includes("py-") || c.includes("rounded"))
|
|
5362
|
+
const hasInteractive = /<Button\b|<button\b|<a\b/.test(code);
|
|
5363
|
+
const hasFocusVisible = /focus-visible:/.test(code);
|
|
5364
|
+
const usesShadcnButton = /import\s.*Button.*from\s+['"]@\/components\/ui\//.test(code);
|
|
5365
|
+
if (hasInteractive && !hasFocusVisible && !usesShadcnButton) {
|
|
5366
|
+
issues.push({
|
|
5367
|
+
line: 0,
|
|
5368
|
+
type: "MISSING_FOCUS_VISIBLE",
|
|
5369
|
+
message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
|
|
5370
|
+
severity: "info"
|
|
5371
|
+
});
|
|
5372
|
+
}
|
|
5373
|
+
const hasTableOrList = /<Table\b|<table\b|\.map\s*\(|<ul\b|<ol\b/.test(code);
|
|
5374
|
+
const hasEmptyCheck = /\.length\s*[=!]==?\s*0|\.length\s*>\s*0|\.length\s*<\s*1|No\s+\w+\s+found|empty|no results|EmptyState|empty state/i.test(
|
|
5375
|
+
code
|
|
5762
5376
|
);
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5377
|
+
if (hasTableOrList && !hasEmptyCheck) {
|
|
5378
|
+
issues.push({
|
|
5379
|
+
line: 0,
|
|
5380
|
+
type: "NO_EMPTY_STATE",
|
|
5381
|
+
message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
|
|
5382
|
+
severity: "warning"
|
|
5383
|
+
});
|
|
5770
5384
|
}
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
if (
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
if (gridPatterns.length > 0) lines.push(`Grids: ${gridPatterns.join(", ")}`);
|
|
5780
|
-
if (lines.length === 0) return "";
|
|
5781
|
-
return `STYLE CONTEXT (match these patterns exactly for visual consistency with the Home page):
|
|
5782
|
-
${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
5783
|
-
}
|
|
5784
|
-
async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
|
|
5785
|
-
let pageNames = [];
|
|
5786
|
-
spinner.start("Phase 1/4 \u2014 Planning pages...");
|
|
5787
|
-
try {
|
|
5788
|
-
const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
|
|
5789
|
-
const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
|
|
5790
|
-
pageNames = pageReqs.map((r) => {
|
|
5791
|
-
const c = r.changes;
|
|
5792
|
-
const name = c.name || c.id || "page";
|
|
5793
|
-
const id = c.id || name.toLowerCase().replace(/\s+/g, "-");
|
|
5794
|
-
const route = c.route || `/${id}`;
|
|
5795
|
-
return { name, id, route };
|
|
5385
|
+
const hasDataFetching = /fetch\s*\(|useQuery|useSWR|useEffect\s*\([^)]*fetch|getData|loadData/i.test(code);
|
|
5386
|
+
const hasLoadingPattern = /skeleton|Skeleton|spinner|Spinner|isLoading|loading|Loading/.test(code);
|
|
5387
|
+
if (hasDataFetching && !hasLoadingPattern) {
|
|
5388
|
+
issues.push({
|
|
5389
|
+
line: 0,
|
|
5390
|
+
type: "NO_LOADING_STATE",
|
|
5391
|
+
message: "Page with data fetching but no loading/skeleton pattern \u2014 add skeleton or spinner",
|
|
5392
|
+
severity: "warning"
|
|
5796
5393
|
});
|
|
5797
|
-
} catch {
|
|
5798
|
-
spinner.text = "AI plan failed \u2014 extracting pages from your request...";
|
|
5799
5394
|
}
|
|
5800
|
-
|
|
5801
|
-
|
|
5395
|
+
const hasGenericError = /Something went wrong|"Error"|'Error'|>Error<\//.test(code) || /error\.message\s*\|\|\s*["']Error["']/.test(code);
|
|
5396
|
+
if (hasGenericError) {
|
|
5397
|
+
issues.push({
|
|
5398
|
+
line: 0,
|
|
5399
|
+
type: "EMPTY_ERROR_MESSAGE",
|
|
5400
|
+
message: "Generic error message detected \u2014 use what happened + why + what to do next",
|
|
5401
|
+
severity: "warning"
|
|
5402
|
+
});
|
|
5802
5403
|
}
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5404
|
+
const hasDestructive = /variant\s*=\s*["']destructive["']|Delete|Remove/.test(code);
|
|
5405
|
+
const hasConfirm = /AlertDialog|Dialog.*confirm|confirm\s*\(|onConfirm|are you sure/i.test(code);
|
|
5406
|
+
if (hasDestructive && !hasConfirm) {
|
|
5407
|
+
issues.push({
|
|
5408
|
+
line: 0,
|
|
5409
|
+
type: "DESTRUCTIVE_NO_CONFIRM",
|
|
5410
|
+
message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
|
|
5411
|
+
severity: "warning"
|
|
5412
|
+
});
|
|
5806
5413
|
}
|
|
5807
|
-
|
|
5808
|
-
const
|
|
5809
|
-
if (!
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5414
|
+
const hasFormSubmit = /<form\b|onSubmit|type\s*=\s*["']submit["']/.test(code);
|
|
5415
|
+
const hasFeedback = /toast|success|error|Saved|Saving|saving|setError|setSuccess/i.test(code);
|
|
5416
|
+
if (hasFormSubmit && !hasFeedback) {
|
|
5417
|
+
issues.push({
|
|
5418
|
+
line: 0,
|
|
5419
|
+
type: "FORM_NO_FEEDBACK",
|
|
5420
|
+
message: 'Form with submit but no success/error feedback pattern \u2014 add "Saving..." then "Saved" or error',
|
|
5421
|
+
severity: "info"
|
|
5422
|
+
});
|
|
5423
|
+
}
|
|
5424
|
+
const hasNav = /<nav\b|NavLink|navigation|sidebar.*link|Sidebar.*link/i.test(code);
|
|
5425
|
+
const hasActiveState = /pathname|active|current|aria-current|data-active/.test(code);
|
|
5426
|
+
if (hasNav && !hasActiveState) {
|
|
5427
|
+
issues.push({
|
|
5428
|
+
line: 0,
|
|
5429
|
+
type: "NAV_NO_ACTIVE_STATE",
|
|
5430
|
+
message: "Navigation without active/current page indicator \u2014 add active state for current route",
|
|
5431
|
+
severity: "info"
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
if (validRoutes && validRoutes.length > 0) {
|
|
5435
|
+
const routeSet = new Set(validRoutes);
|
|
5436
|
+
routeSet.add("#");
|
|
5437
|
+
const lines = code.split("\n");
|
|
5438
|
+
const linkHrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
|
|
5439
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5440
|
+
let match;
|
|
5441
|
+
while ((match = linkHrefRe.exec(lines[i])) !== null) {
|
|
5442
|
+
const target = match[1];
|
|
5443
|
+
if (target === "/" || target.startsWith("/design-system") || target.startsWith("/api") || target.startsWith("/#"))
|
|
5444
|
+
continue;
|
|
5445
|
+
if (!routeSet.has(target)) {
|
|
5446
|
+
issues.push({
|
|
5447
|
+
line: i + 1,
|
|
5448
|
+
type: "BROKEN_INTERNAL_LINK",
|
|
5449
|
+
message: `Link to "${target}" \u2014 route does not exist in project`,
|
|
5450
|
+
severity: "warning"
|
|
5451
|
+
});
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
const linkBlockRe = /<(?:Link|a)\b[^>]*>[\s\S]*?<\/(?:Link|a)>/g;
|
|
5457
|
+
let linkMatch;
|
|
5458
|
+
while ((linkMatch = linkBlockRe.exec(code)) !== null) {
|
|
5459
|
+
const block = linkMatch[0];
|
|
5460
|
+
if (/<(?:Button|button)\b/.test(block) && !/asChild/.test(block)) {
|
|
5461
|
+
issues.push({
|
|
5462
|
+
line: 0,
|
|
5463
|
+
type: "NESTED_INTERACTIVE",
|
|
5464
|
+
message: "Button inside Link without asChild \u2014 causes DOM nesting error. Use <Button asChild><Link>...</Link></Button> instead",
|
|
5465
|
+
severity: "error"
|
|
5466
|
+
});
|
|
5467
|
+
break;
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
const nestedAnchorRe = /<a\b[^>]*>[\s\S]*?<a\b/;
|
|
5471
|
+
if (nestedAnchorRe.test(code)) {
|
|
5472
|
+
issues.push({
|
|
5473
|
+
line: 0,
|
|
5474
|
+
type: "NESTED_INTERACTIVE",
|
|
5475
|
+
message: "Nested <a> tags \u2014 causes DOM nesting error. Remove inner anchor or restructure",
|
|
5476
|
+
severity: "error"
|
|
5477
|
+
});
|
|
5478
|
+
}
|
|
5479
|
+
return issues;
|
|
5480
|
+
}
|
|
5481
|
+
function replaceRawColors(classes, colorMap) {
|
|
5482
|
+
let changed = false;
|
|
5483
|
+
let result = classes;
|
|
5484
|
+
const accentColorRe = /\b(bg|text|border)-(emerald|blue|violet|indigo|purple|teal|cyan|sky|rose|amber|red|green|yellow|pink|orange|fuchsia|lime)-(\d+)\b/g;
|
|
5485
|
+
result = result.replace(accentColorRe, (m, prefix, color, shade) => {
|
|
5486
|
+
if (colorMap[m]) {
|
|
5487
|
+
changed = true;
|
|
5488
|
+
return colorMap[m];
|
|
5489
|
+
}
|
|
5490
|
+
const n = parseInt(shade);
|
|
5491
|
+
const isDestructive = color === "red";
|
|
5492
|
+
if (prefix === "bg") {
|
|
5493
|
+
if (n >= 500 && n <= 700) {
|
|
5494
|
+
changed = true;
|
|
5495
|
+
return isDestructive ? "bg-destructive" : "bg-primary";
|
|
5496
|
+
}
|
|
5497
|
+
if (n >= 100 && n <= 200) {
|
|
5498
|
+
changed = true;
|
|
5499
|
+
return isDestructive ? "bg-destructive/10" : "bg-primary/10";
|
|
5500
|
+
}
|
|
5501
|
+
if (n >= 300 && n <= 400) {
|
|
5502
|
+
changed = true;
|
|
5503
|
+
return isDestructive ? "bg-destructive/20" : "bg-primary/20";
|
|
5504
|
+
}
|
|
5505
|
+
if (n >= 800) {
|
|
5506
|
+
changed = true;
|
|
5507
|
+
return "bg-muted";
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
if (prefix === "text") {
|
|
5511
|
+
if (n >= 400 && n <= 600) {
|
|
5512
|
+
changed = true;
|
|
5513
|
+
return isDestructive ? "text-destructive" : "text-primary";
|
|
5514
|
+
}
|
|
5515
|
+
if (n >= 100 && n <= 300) {
|
|
5516
|
+
changed = true;
|
|
5517
|
+
return "text-foreground";
|
|
5518
|
+
}
|
|
5519
|
+
if (n >= 700) {
|
|
5520
|
+
changed = true;
|
|
5521
|
+
return "text-foreground";
|
|
5522
|
+
}
|
|
5523
|
+
}
|
|
5524
|
+
if (prefix === "border") {
|
|
5525
|
+
changed = true;
|
|
5526
|
+
return isDestructive ? "border-destructive" : "border-primary";
|
|
5816
5527
|
}
|
|
5528
|
+
return m;
|
|
5529
|
+
});
|
|
5530
|
+
const neutralColorRe = /\b(bg|text|border)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
|
|
5531
|
+
result = result.replace(neutralColorRe, (m, prefix, _color, shade) => {
|
|
5532
|
+
if (colorMap[m]) {
|
|
5533
|
+
changed = true;
|
|
5534
|
+
return colorMap[m];
|
|
5535
|
+
}
|
|
5536
|
+
const n = parseInt(shade);
|
|
5537
|
+
if (prefix === "bg") {
|
|
5538
|
+
if (n >= 800) {
|
|
5539
|
+
changed = true;
|
|
5540
|
+
return "bg-background";
|
|
5541
|
+
}
|
|
5542
|
+
if (n >= 100 && n <= 300) {
|
|
5543
|
+
changed = true;
|
|
5544
|
+
return "bg-muted";
|
|
5545
|
+
}
|
|
5546
|
+
}
|
|
5547
|
+
if (prefix === "text") {
|
|
5548
|
+
if (n >= 100 && n <= 300) {
|
|
5549
|
+
changed = true;
|
|
5550
|
+
return "text-foreground";
|
|
5551
|
+
}
|
|
5552
|
+
if (n >= 400 && n <= 600) {
|
|
5553
|
+
changed = true;
|
|
5554
|
+
return "text-muted-foreground";
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
if (prefix === "border") {
|
|
5558
|
+
changed = true;
|
|
5559
|
+
return "border-border";
|
|
5560
|
+
}
|
|
5561
|
+
return m;
|
|
5562
|
+
});
|
|
5563
|
+
return { result, changed };
|
|
5564
|
+
}
|
|
5565
|
+
async function autoFixCode(code) {
|
|
5566
|
+
const fixes = [];
|
|
5567
|
+
let fixed = code;
|
|
5568
|
+
const beforeQuoteFix = fixed;
|
|
5569
|
+
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
5570
|
+
if (fixed !== beforeQuoteFix) {
|
|
5571
|
+
fixes.push("fixed escaped closing quotes in strings");
|
|
5817
5572
|
}
|
|
5818
|
-
const
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
|
|
5822
|
-
|
|
5573
|
+
const beforeEntityFix = fixed;
|
|
5574
|
+
fixed = fixed.replace(/<=/g, "<=");
|
|
5575
|
+
fixed = fixed.replace(/>=/g, ">=");
|
|
5576
|
+
fixed = fixed.replace(/&&/g, "&&");
|
|
5577
|
+
fixed = fixed.replace(/([\w)\]])\s*<\s*([\w(])/g, "$1 < $2");
|
|
5578
|
+
fixed = fixed.replace(/([\w)\]])\s*>\s*([\w(])/g, "$1 > $2");
|
|
5579
|
+
if (fixed !== beforeEntityFix) {
|
|
5580
|
+
fixes.push("Fixed syntax issues");
|
|
5823
5581
|
}
|
|
5824
|
-
const
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
const homePage = homeIdx !== -1 ? pageNames[homeIdx] : pageNames[0];
|
|
5830
|
-
const remainingPages = pageNames.filter((_, i) => i !== (homeIdx !== -1 ? homeIdx : 0));
|
|
5831
|
-
spinner.start(`Phase 2/4 \u2014 Generating ${homePage.name} page (sets design direction)...`);
|
|
5832
|
-
let homeRequest = null;
|
|
5833
|
-
let homePageCode = "";
|
|
5834
|
-
try {
|
|
5835
|
-
const homeResult = await parseModification(
|
|
5836
|
-
`Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This REPLACES the default placeholder page \u2014 generate a complete, content-rich landing page for the project described above. Generate complete pageCode. Include a branded site-wide <header> with navigation links to ALL these pages: ${allPagesList}. Use these EXACT routes in navigation: ${allRoutes}. Include a <footer> at the bottom. Make it visually polished \u2014 this page sets the design direction for the entire site. Do not generate other pages.`,
|
|
5837
|
-
modCtx,
|
|
5838
|
-
provider,
|
|
5839
|
-
parseOpts
|
|
5840
|
-
);
|
|
5841
|
-
const codePage = homeResult.requests.find((r) => r.type === "add-page");
|
|
5842
|
-
if (codePage) {
|
|
5843
|
-
homeRequest = codePage;
|
|
5844
|
-
homePageCode = codePage.changes?.pageCode || "";
|
|
5845
|
-
}
|
|
5846
|
-
} catch {
|
|
5582
|
+
const beforeLtFix = fixed;
|
|
5583
|
+
fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1<$2");
|
|
5584
|
+
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$2");
|
|
5585
|
+
if (fixed !== beforeLtFix) {
|
|
5586
|
+
fixes.push("escaped < in JSX text content");
|
|
5847
5587
|
}
|
|
5848
|
-
if (
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
target: "new",
|
|
5852
|
-
changes: { id: homePage.id, name: homePage.name, route: homePage.route }
|
|
5853
|
-
};
|
|
5588
|
+
if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
|
|
5589
|
+
fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
|
|
5590
|
+
fixes.push("text-base \u2192 text-sm");
|
|
5854
5591
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
if (styleContext) {
|
|
5859
|
-
const lineCount = styleContext.split("\n").length - 1;
|
|
5860
|
-
spinner.succeed(`Phase 3/4 \u2014 Extracted ${lineCount} style patterns from ${homePage.name}`);
|
|
5861
|
-
} else {
|
|
5862
|
-
spinner.succeed("Phase 3/4 \u2014 No style patterns extracted (Home page had no code)");
|
|
5592
|
+
if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
|
|
5593
|
+
fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
|
|
5594
|
+
fixes.push("large text in CardTitle \u2192 removed");
|
|
5863
5595
|
}
|
|
5864
|
-
if (
|
|
5865
|
-
|
|
5596
|
+
if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
|
|
5597
|
+
fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
|
|
5598
|
+
fixes.push("heavy shadow \u2192 shadow-sm");
|
|
5866
5599
|
}
|
|
5867
|
-
|
|
5868
|
-
const
|
|
5869
|
-
|
|
5870
|
-
const alignmentNote = 'CRITICAL LAYOUT RULE: Every <section> must wrap its content in a container div matching the header width. Use the EXACT same container classes as shown in the style context (e.g. className="container max-w-6xl px-4" or className="max-w-6xl mx-auto px-4"). Inner content can use narrower max-w for text centering, but the outer section container MUST match.';
|
|
5871
|
-
const existingPagesContext = buildExistingPagesContext(modCtx.config);
|
|
5872
|
-
const AI_CONCURRENCY = 3;
|
|
5873
|
-
let phase4Done = 0;
|
|
5874
|
-
const remainingRequests = await pMap(
|
|
5875
|
-
remainingPages,
|
|
5876
|
-
async ({ name, id, route }) => {
|
|
5877
|
-
const prompt = [
|
|
5878
|
-
`Create ONE page called "${name}" at route "${route}".`,
|
|
5879
|
-
`Context: ${message}.`,
|
|
5880
|
-
`Generate complete pageCode for this single page only. Do not generate other pages.`,
|
|
5881
|
-
sharedNote,
|
|
5882
|
-
routeNote,
|
|
5883
|
-
alignmentNote,
|
|
5884
|
-
existingPagesContext,
|
|
5885
|
-
styleContext
|
|
5886
|
-
].filter(Boolean).join("\n\n");
|
|
5887
|
-
try {
|
|
5888
|
-
const result = await parseModification(prompt, modCtx, provider, parseOpts);
|
|
5889
|
-
phase4Done++;
|
|
5890
|
-
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
5891
|
-
const codePage = result.requests.find((r) => r.type === "add-page");
|
|
5892
|
-
return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
|
|
5893
|
-
} catch {
|
|
5894
|
-
phase4Done++;
|
|
5895
|
-
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
5896
|
-
return { type: "add-page", target: "new", changes: { id, name, route } };
|
|
5897
|
-
}
|
|
5898
|
-
},
|
|
5899
|
-
AI_CONCURRENCY
|
|
5600
|
+
const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
|
|
5601
|
+
const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
|
|
5602
|
+
fixed
|
|
5900
5603
|
);
|
|
5901
|
-
const
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
return allRequests;
|
|
5905
|
-
}
|
|
5906
|
-
|
|
5907
|
-
// src/commands/chat/modification-handler.ts
|
|
5908
|
-
import { resolve as resolve7 } from "path";
|
|
5909
|
-
import { mkdir as mkdir4 } from "fs/promises";
|
|
5910
|
-
import { dirname as dirname6 } from "path";
|
|
5911
|
-
import chalk11 from "chalk";
|
|
5912
|
-
import {
|
|
5913
|
-
getTemplateForPageType,
|
|
5914
|
-
loadManifest as loadManifest5,
|
|
5915
|
-
saveManifest,
|
|
5916
|
-
updateUsedIn,
|
|
5917
|
-
findSharedComponentByIdOrName,
|
|
5918
|
-
generateSharedComponent as generateSharedComponent3
|
|
5919
|
-
} from "@getcoherent/core";
|
|
5604
|
+
const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
|
|
5605
|
+
if ((hasHooks || hasEvents) && !hasUseClient) {
|
|
5606
|
+
fixed = `'use client'
|
|
5920
5607
|
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
5924
|
-
var TEXT_BASE_RE = /\btext-base\b/g;
|
|
5925
|
-
var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
|
|
5926
|
-
var SM_BREAKPOINT_RE = /\bsm:/g;
|
|
5927
|
-
var XL_BREAKPOINT_RE = /\bxl:/g;
|
|
5928
|
-
var XXL_BREAKPOINT_RE = /\b2xl:/g;
|
|
5929
|
-
var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
|
|
5930
|
-
var RAW_BUTTON_RE = /<button\b/g;
|
|
5931
|
-
var RAW_INPUT_RE = /<input\b/g;
|
|
5932
|
-
var RAW_SELECT_RE = /<select\b/g;
|
|
5933
|
-
var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
|
|
5934
|
-
var NATIVE_TABLE_RE = /<table\b/g;
|
|
5935
|
-
var PLACEHOLDER_PATTERNS = [
|
|
5936
|
-
/>\s*Lorem ipsum\b/i,
|
|
5937
|
-
/>\s*Card content\s*</i,
|
|
5938
|
-
/>\s*Your (?:text|content) here\s*</i,
|
|
5939
|
-
/>\s*Description\s*</,
|
|
5940
|
-
/>\s*Title\s*</,
|
|
5941
|
-
/placeholder\s*text/i
|
|
5942
|
-
];
|
|
5943
|
-
var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
|
|
5944
|
-
var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
|
|
5945
|
-
var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
|
|
5946
|
-
var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
|
|
5947
|
-
function isInsideCommentOrString(line, matchIndex) {
|
|
5948
|
-
const commentIdx = line.indexOf("//");
|
|
5949
|
-
if (commentIdx !== -1 && commentIdx < matchIndex) return true;
|
|
5950
|
-
let inSingle = false;
|
|
5951
|
-
let inDouble = false;
|
|
5952
|
-
let inTemplate = false;
|
|
5953
|
-
for (let i = 0; i < matchIndex; i++) {
|
|
5954
|
-
const ch = line[i];
|
|
5955
|
-
const prev = i > 0 ? line[i - 1] : "";
|
|
5956
|
-
if (prev === "\\") continue;
|
|
5957
|
-
if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
|
|
5958
|
-
if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
|
|
5959
|
-
if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
|
|
5608
|
+
${fixed}`;
|
|
5609
|
+
fixes.push('added "use client" (client features detected)');
|
|
5960
5610
|
}
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
const endIdx = line.indexOf("*/");
|
|
5972
|
-
if (endIdx !== -1) {
|
|
5973
|
-
inBlockComment = false;
|
|
5974
|
-
}
|
|
5975
|
-
continue;
|
|
5976
|
-
}
|
|
5977
|
-
const blockStart = line.indexOf("/*");
|
|
5978
|
-
if (blockStart !== -1 && !line.includes("*/")) {
|
|
5979
|
-
inBlockComment = true;
|
|
5980
|
-
continue;
|
|
5611
|
+
if (/^['"]use client['"]/.test(fixed.trim()) && /\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/.test(fixed)) {
|
|
5612
|
+
const metaMatch = fixed.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
|
|
5613
|
+
if (metaMatch) {
|
|
5614
|
+
const start = fixed.indexOf(metaMatch[0]);
|
|
5615
|
+
const open = fixed.indexOf("{", start);
|
|
5616
|
+
let depth = 1, i = open + 1;
|
|
5617
|
+
while (i < fixed.length && depth > 0) {
|
|
5618
|
+
if (fixed[i] === "{") depth++;
|
|
5619
|
+
else if (fixed[i] === "}") depth--;
|
|
5620
|
+
i++;
|
|
5981
5621
|
}
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5622
|
+
const tail = fixed.slice(i);
|
|
5623
|
+
const semi = tail.match(/^\s*;/);
|
|
5624
|
+
const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
|
|
5625
|
+
fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
|
|
5626
|
+
fixes.push('removed metadata export (conflicts with "use client")');
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
const lines = fixed.split("\n");
|
|
5630
|
+
let hasReplacedButton = false;
|
|
5631
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5632
|
+
if (!/<button\b/.test(lines[i])) continue;
|
|
5633
|
+
if (lines[i].includes("aria-label")) continue;
|
|
5634
|
+
if (/onClick=\{.*copy/i.test(lines[i])) continue;
|
|
5635
|
+
const block = lines.slice(i, i + 5).join(" ");
|
|
5636
|
+
if (block.includes("aria-label") || /onClick=\{.*copy/i.test(block)) continue;
|
|
5637
|
+
lines[i] = lines[i].replace(/<button\b/g, "<Button");
|
|
5638
|
+
hasReplacedButton = true;
|
|
5639
|
+
}
|
|
5640
|
+
if (hasReplacedButton) {
|
|
5641
|
+
fixed = lines.join("\n");
|
|
5642
|
+
fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
|
|
5643
|
+
return "</Button>";
|
|
5644
|
+
});
|
|
5645
|
+
const openCount = (fixed.match(/<Button\b/g) || []).length;
|
|
5646
|
+
const closeCount = (fixed.match(/<\/Button>/g) || []).length;
|
|
5647
|
+
if (closeCount > openCount) {
|
|
5648
|
+
let excess = closeCount - openCount;
|
|
5649
|
+
fixed = fixed.replace(/<\/Button>/g, (m) => {
|
|
5650
|
+
if (excess > 0) {
|
|
5651
|
+
excess--;
|
|
5652
|
+
return "</button>";
|
|
5988
5653
|
}
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5654
|
+
return m;
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
const hasButtonImport = /import\s.*\bButton\b.*from\s+['"]@\/components\/ui\/button['"]/.test(fixed);
|
|
5658
|
+
if (!hasButtonImport) {
|
|
5659
|
+
const lastImportIdx = fixed.lastIndexOf("\nimport ");
|
|
5660
|
+
if (lastImportIdx !== -1) {
|
|
5661
|
+
const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
|
|
5662
|
+
fixed = fixed.slice(0, lineEnd + 1) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(lineEnd + 1);
|
|
5663
|
+
} else {
|
|
5664
|
+
const insertAfter = hasUseClient ? fixed.indexOf("\n") + 1 : 0;
|
|
5665
|
+
fixed = fixed.slice(0, insertAfter) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(insertAfter);
|
|
5994
5666
|
}
|
|
5995
5667
|
}
|
|
5668
|
+
fixes.push("<button> \u2192 <Button> (with import)");
|
|
5996
5669
|
}
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
5670
|
+
const colorMap = {
|
|
5671
|
+
"bg-zinc-950": "bg-background",
|
|
5672
|
+
"bg-zinc-900": "bg-background",
|
|
5673
|
+
"bg-slate-950": "bg-background",
|
|
5674
|
+
"bg-slate-900": "bg-background",
|
|
5675
|
+
"bg-gray-950": "bg-background",
|
|
5676
|
+
"bg-gray-900": "bg-background",
|
|
5677
|
+
"bg-zinc-800": "bg-muted",
|
|
5678
|
+
"bg-slate-800": "bg-muted",
|
|
5679
|
+
"bg-gray-800": "bg-muted",
|
|
5680
|
+
"bg-zinc-100": "bg-muted",
|
|
5681
|
+
"bg-slate-100": "bg-muted",
|
|
5682
|
+
"bg-gray-100": "bg-muted",
|
|
5683
|
+
"bg-white": "bg-background",
|
|
5684
|
+
"bg-black": "bg-background",
|
|
5685
|
+
"text-white": "text-foreground",
|
|
5686
|
+
"text-black": "text-foreground",
|
|
5687
|
+
"text-zinc-100": "text-foreground",
|
|
5688
|
+
"text-zinc-200": "text-foreground",
|
|
5689
|
+
"text-slate-100": "text-foreground",
|
|
5690
|
+
"text-gray-100": "text-foreground",
|
|
5691
|
+
"text-zinc-400": "text-muted-foreground",
|
|
5692
|
+
"text-zinc-500": "text-muted-foreground",
|
|
5693
|
+
"text-slate-400": "text-muted-foreground",
|
|
5694
|
+
"text-slate-500": "text-muted-foreground",
|
|
5695
|
+
"text-gray-400": "text-muted-foreground",
|
|
5696
|
+
"text-gray-500": "text-muted-foreground",
|
|
5697
|
+
"border-zinc-700": "border-border",
|
|
5698
|
+
"border-zinc-800": "border-border",
|
|
5699
|
+
"border-slate-700": "border-border",
|
|
5700
|
+
"border-gray-700": "border-border",
|
|
5701
|
+
"border-zinc-200": "border-border",
|
|
5702
|
+
"border-slate-200": "border-border",
|
|
5703
|
+
"border-gray-200": "border-border"
|
|
6008
5704
|
};
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
);
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
)
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
);
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
if (
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
5705
|
+
const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
|
|
5706
|
+
const isInsideTerminalBlock = (offset) => {
|
|
5707
|
+
const preceding = fixed.slice(Math.max(0, offset - 600), offset);
|
|
5708
|
+
if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
|
|
5709
|
+
if (!/font-mono/.test(preceding)) return false;
|
|
5710
|
+
const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
|
|
5711
|
+
const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
|
|
5712
|
+
return lastTerminal > lastClose;
|
|
5713
|
+
};
|
|
5714
|
+
let hadColorFix = false;
|
|
5715
|
+
fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
|
|
5716
|
+
if (isCodeContext(classes)) return fullMatch;
|
|
5717
|
+
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
5718
|
+
const { result, changed } = replaceRawColors(classes, colorMap);
|
|
5719
|
+
if (changed) hadColorFix = true;
|
|
5720
|
+
if (result !== classes) return `className="${result}"`;
|
|
5721
|
+
return fullMatch;
|
|
5722
|
+
});
|
|
5723
|
+
fixed = fixed.replace(/(?:cn|clsx|cva)\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g, (fullMatch, args) => {
|
|
5724
|
+
const replaced = args.replace(/"([^"]*)"/g, (_qm, inner) => {
|
|
5725
|
+
const { result, changed } = replaceRawColors(inner, colorMap);
|
|
5726
|
+
if (changed) hadColorFix = true;
|
|
5727
|
+
return `"${result}"`;
|
|
5728
|
+
});
|
|
5729
|
+
if (replaced !== args) return fullMatch.replace(args, replaced);
|
|
5730
|
+
return fullMatch;
|
|
5731
|
+
});
|
|
5732
|
+
fixed = fixed.replace(/className='([^']*)'/g, (fullMatch, classes, offset) => {
|
|
5733
|
+
if (isCodeContext(classes)) return fullMatch;
|
|
5734
|
+
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
5735
|
+
const { result, changed } = replaceRawColors(classes, colorMap);
|
|
5736
|
+
if (changed) hadColorFix = true;
|
|
5737
|
+
if (result !== classes) return `className='${result}'`;
|
|
5738
|
+
return fullMatch;
|
|
5739
|
+
});
|
|
5740
|
+
fixed = fixed.replace(/className=\{`([^`]*)`\}/g, (fullMatch, inner) => {
|
|
5741
|
+
const { result, changed } = replaceRawColors(inner, colorMap);
|
|
5742
|
+
if (changed) hadColorFix = true;
|
|
5743
|
+
if (result !== inner) return `className={\`${result}\`}`;
|
|
5744
|
+
return fullMatch;
|
|
5745
|
+
});
|
|
5746
|
+
if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
|
|
5747
|
+
const selectRe = /<select\b[^>]*>([\s\S]*?)<\/select>/g;
|
|
5748
|
+
let hadSelectFix = false;
|
|
5749
|
+
fixed = fixed.replace(selectRe, (_match, inner) => {
|
|
5750
|
+
const options = [];
|
|
5751
|
+
const optionRe = /<option\s+value="([^"]*)"[^>]*>([^<]*)<\/option>/g;
|
|
5752
|
+
let optMatch;
|
|
5753
|
+
while ((optMatch = optionRe.exec(inner)) !== null) {
|
|
5754
|
+
options.push({ value: optMatch[1], label: optMatch[2] });
|
|
5755
|
+
}
|
|
5756
|
+
if (options.length === 0) return _match;
|
|
5757
|
+
hadSelectFix = true;
|
|
5758
|
+
const items = options.map((o) => ` <SelectItem value="${o.value}">${o.label}</SelectItem>`).join("\n");
|
|
5759
|
+
return `<Select>
|
|
5760
|
+
<SelectTrigger>
|
|
5761
|
+
<SelectValue placeholder="Select..." />
|
|
5762
|
+
</SelectTrigger>
|
|
5763
|
+
<SelectContent>
|
|
5764
|
+
${items}
|
|
5765
|
+
</SelectContent>
|
|
5766
|
+
</Select>`;
|
|
5767
|
+
});
|
|
5768
|
+
if (hadSelectFix) {
|
|
5769
|
+
fixes.push("<select> \u2192 shadcn Select");
|
|
5770
|
+
const selectImport = `import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'`;
|
|
5771
|
+
if (!/from\s+['"]@\/components\/ui\/select['"]/.test(fixed)) {
|
|
5772
|
+
const replaced = fixed.replace(
|
|
5773
|
+
/(import\s+\{[^}]*\}\s+from\s+['"]@\/components\/ui\/[^'"]+['"])/,
|
|
5774
|
+
`$1
|
|
5775
|
+
${selectImport}`
|
|
5776
|
+
);
|
|
5777
|
+
if (replaced !== fixed) {
|
|
5778
|
+
fixed = replaced;
|
|
5779
|
+
} else {
|
|
5780
|
+
fixed = selectImport + "\n" + fixed;
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
5785
|
+
if (lucideImportMatch) {
|
|
5786
|
+
let lucideExports = null;
|
|
5787
|
+
try {
|
|
5788
|
+
const { createRequire } = await import("module");
|
|
5789
|
+
const require2 = createRequire(process.cwd() + "/package.json");
|
|
5790
|
+
const lr = require2("lucide-react");
|
|
5791
|
+
lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
5792
|
+
} catch {
|
|
5793
|
+
}
|
|
5794
|
+
if (lucideExports) {
|
|
5795
|
+
const nonLucideImports = /* @__PURE__ */ new Set();
|
|
5796
|
+
for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from\s*["'](?!lucide-react)([^"']+)["']/g)) {
|
|
5797
|
+
m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => nonLucideImports.add(n));
|
|
5798
|
+
}
|
|
5799
|
+
const iconNames = lucideImportMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
5800
|
+
const duplicates = iconNames.filter((name) => nonLucideImports.has(name));
|
|
5801
|
+
let newImport = lucideImportMatch[1];
|
|
5802
|
+
for (const dup of duplicates) {
|
|
5803
|
+
newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
|
|
5804
|
+
fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
|
|
5805
|
+
}
|
|
5806
|
+
const invalid = iconNames.filter((name) => !lucideExports.has(name) && !nonLucideImports.has(name));
|
|
5807
|
+
if (invalid.length > 0) {
|
|
5808
|
+
const fallback = "Circle";
|
|
5809
|
+
for (const bad of invalid) {
|
|
5810
|
+
const re = new RegExp(`\\b${bad}\\b`, "g");
|
|
5811
|
+
newImport = newImport.replace(re, fallback);
|
|
5812
|
+
fixed = fixed.replace(re, fallback);
|
|
5813
|
+
}
|
|
5814
|
+
fixes.push(`invalid lucide icons \u2192 ${fallback}: ${invalid.join(", ")}`);
|
|
5815
|
+
}
|
|
5816
|
+
if (duplicates.length > 0 || invalid.length > 0) {
|
|
5817
|
+
const importedNames = [
|
|
5818
|
+
...new Set(
|
|
5819
|
+
newImport.split(",").map((s) => s.trim()).filter(Boolean)
|
|
5820
|
+
)
|
|
5821
|
+
];
|
|
5822
|
+
const originalImportLine = lucideImportMatch[0];
|
|
5823
|
+
fixed = fixed.replace(originalImportLine, `import { ${importedNames.join(", ")} } from "lucide-react"`);
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
6127
5826
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
5827
|
+
const lucideImportMatch2 = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
5828
|
+
if (lucideImportMatch2) {
|
|
5829
|
+
let lucideExports2 = null;
|
|
5830
|
+
try {
|
|
5831
|
+
const { createRequire } = await import("module");
|
|
5832
|
+
const req = createRequire(process.cwd() + "/package.json");
|
|
5833
|
+
const lr = req("lucide-react");
|
|
5834
|
+
lucideExports2 = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
5835
|
+
} catch {
|
|
5836
|
+
}
|
|
5837
|
+
if (lucideExports2) {
|
|
5838
|
+
const allImportedNames = /* @__PURE__ */ new Set();
|
|
5839
|
+
for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
|
|
5840
|
+
m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => allImportedNames.add(n));
|
|
5841
|
+
}
|
|
5842
|
+
for (const m of fixed.matchAll(/import\s+([A-Z]\w+)\s+from/g)) {
|
|
5843
|
+
allImportedNames.add(m[1]);
|
|
5844
|
+
}
|
|
5845
|
+
const lucideImported = new Set(
|
|
5846
|
+
lucideImportMatch2[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
5847
|
+
);
|
|
5848
|
+
const jsxIconRefs = [...new Set([...fixed.matchAll(/<([A-Z][a-zA-Z]*Icon)\s/g)].map((m) => m[1]))];
|
|
5849
|
+
const missing = [];
|
|
5850
|
+
for (const ref of jsxIconRefs) {
|
|
5851
|
+
if (allImportedNames.has(ref)) continue;
|
|
5852
|
+
if (fixed.includes(`function ${ref}`) || fixed.includes(`const ${ref}`)) continue;
|
|
5853
|
+
const baseName = ref.replace(/Icon$/, "");
|
|
5854
|
+
if (lucideExports2.has(ref)) {
|
|
5855
|
+
missing.push(ref);
|
|
5856
|
+
lucideImported.add(ref);
|
|
5857
|
+
} else if (lucideExports2.has(baseName)) {
|
|
5858
|
+
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
5859
|
+
fixed = fixed.replace(re, baseName);
|
|
5860
|
+
missing.push(baseName);
|
|
5861
|
+
lucideImported.add(baseName);
|
|
5862
|
+
fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
|
|
5863
|
+
} else {
|
|
5864
|
+
const fallback = "Circle";
|
|
5865
|
+
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
5866
|
+
fixed = fixed.replace(re, fallback);
|
|
5867
|
+
lucideImported.add(fallback);
|
|
5868
|
+
fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
if (missing.length > 0) {
|
|
5872
|
+
const allNames = [...lucideImported];
|
|
5873
|
+
const origLine = lucideImportMatch2[0];
|
|
5874
|
+
fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
|
|
5875
|
+
fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
|
|
6138
5876
|
}
|
|
6139
5877
|
}
|
|
6140
5878
|
}
|
|
6141
|
-
const
|
|
6142
|
-
const
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
5879
|
+
const linkWithButtonRe = /(<Link\b[^>]*>)\s*(<Button\b(?![^>]*asChild)[^>]*>)([\s\S]*?)<\/Button>\s*<\/Link>/g;
|
|
5880
|
+
const beforeLinkFix = fixed;
|
|
5881
|
+
fixed = fixed.replace(linkWithButtonRe, (_match, linkOpen, buttonOpen, inner) => {
|
|
5882
|
+
const hrefMatch = linkOpen.match(/href="([^"]*)"/);
|
|
5883
|
+
const href = hrefMatch ? hrefMatch[1] : "/";
|
|
5884
|
+
const buttonWithAsChild = buttonOpen.replace("<Button", "<Button asChild");
|
|
5885
|
+
return `${buttonWithAsChild}<Link href="${href}">${inner.trim()}</Link></Button>`;
|
|
5886
|
+
});
|
|
5887
|
+
if (fixed !== beforeLinkFix) {
|
|
5888
|
+
fixes.push("Link>Button \u2192 Button asChild>Link (DOM nesting fix)");
|
|
5889
|
+
}
|
|
5890
|
+
fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
|
|
5891
|
+
const cleaned = inner.replace(/\s{2,}/g, " ").trim();
|
|
5892
|
+
return `className="${cleaned}"`;
|
|
5893
|
+
});
|
|
5894
|
+
let imgCounter = 1;
|
|
5895
|
+
const beforeImgFix = fixed;
|
|
5896
|
+
fixed = fixed.replace(/["']\/api\/placeholder\/(\d+)\/(\d+)["']/g, (_m, w, h) => {
|
|
5897
|
+
return `"https://picsum.photos/${w}/${h}?random=${imgCounter++}"`;
|
|
5898
|
+
});
|
|
5899
|
+
fixed = fixed.replace(/["']\/placeholder-avatar[^"']*["']/g, () => {
|
|
5900
|
+
return `"https://i.pravatar.cc/150?u=user${imgCounter++}"`;
|
|
5901
|
+
});
|
|
5902
|
+
fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
|
|
5903
|
+
const height = h || w;
|
|
5904
|
+
return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
|
|
5905
|
+
});
|
|
5906
|
+
fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
|
|
5907
|
+
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
5908
|
+
});
|
|
5909
|
+
fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
|
|
5910
|
+
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
5911
|
+
});
|
|
5912
|
+
if (fixed !== beforeImgFix) {
|
|
5913
|
+
fixes.push("placeholder images \u2192 working URLs (picsum/pravatar)");
|
|
5914
|
+
}
|
|
5915
|
+
return { code: fixed, fixes };
|
|
5916
|
+
}
|
|
5917
|
+
function formatIssues(issues) {
|
|
5918
|
+
if (issues.length === 0) return "";
|
|
5919
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
5920
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
5921
|
+
const infos = issues.filter((i) => i.severity === "info");
|
|
5922
|
+
const lines = [];
|
|
5923
|
+
if (errors.length > 0) {
|
|
5924
|
+
lines.push(` \u274C ${errors.length} error(s):`);
|
|
5925
|
+
for (const e of errors) {
|
|
5926
|
+
lines.push(` L${e.line}: [${e.type}] ${e.message}`);
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
if (warnings.length > 0) {
|
|
5930
|
+
lines.push(` \u26A0\uFE0F ${warnings.length} warning(s):`);
|
|
5931
|
+
for (const w of warnings) {
|
|
5932
|
+
lines.push(` L${w.line}: [${w.type}] ${w.message}`);
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
if (infos.length > 0) {
|
|
5936
|
+
lines.push(` \u2139\uFE0F ${infos.length} info:`);
|
|
5937
|
+
for (const i of infos) {
|
|
5938
|
+
lines.push(` L${i.line}: [${i.type}] ${i.message}`);
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
return lines.join("\n");
|
|
5942
|
+
}
|
|
5943
|
+
function checkDesignConsistency(code) {
|
|
5944
|
+
const warnings = [];
|
|
5945
|
+
const hexPattern = /\[#[0-9a-fA-F]{3,8}\]/g;
|
|
5946
|
+
for (const match of code.matchAll(hexPattern)) {
|
|
5947
|
+
warnings.push({
|
|
5948
|
+
type: "hardcoded-color",
|
|
5949
|
+
message: `Hardcoded color ${match[0]} \u2014 use a design token (e.g., bg-primary) instead`
|
|
6149
5950
|
});
|
|
6150
5951
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
"GENERIC_BUTTON_TEXT",
|
|
6165
|
-
'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
|
|
6166
|
-
"warning"
|
|
6167
|
-
)
|
|
6168
|
-
);
|
|
6169
|
-
const h1Matches = code.match(/<h1[\s>]/g);
|
|
6170
|
-
if (!h1Matches || h1Matches.length === 0) {
|
|
5952
|
+
const spacingPattern = /[pm][trblxy]?-\[\d+px\]/g;
|
|
5953
|
+
for (const match of code.matchAll(spacingPattern)) {
|
|
5954
|
+
warnings.push({
|
|
5955
|
+
type: "arbitrary-spacing",
|
|
5956
|
+
message: `Arbitrary spacing ${match[0]} \u2014 use Tailwind spacing scale instead`
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
return warnings;
|
|
5960
|
+
}
|
|
5961
|
+
function verifyIncrementalEdit(before, after) {
|
|
5962
|
+
const issues = [];
|
|
5963
|
+
const hookPattern = /\buse[A-Z]\w+\s*\(/;
|
|
5964
|
+
if (hookPattern.test(after) && !after.includes("'use client'") && !after.includes('"use client"')) {
|
|
6171
5965
|
issues.push({
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
|
|
6175
|
-
severity: "warning"
|
|
5966
|
+
type: "missing-use-client",
|
|
5967
|
+
message: 'Code uses React hooks but missing "use client" directive'
|
|
6176
5968
|
});
|
|
6177
|
-
}
|
|
5969
|
+
}
|
|
5970
|
+
if (!after.includes("export default")) {
|
|
6178
5971
|
issues.push({
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
|
|
6182
|
-
severity: "warning"
|
|
5972
|
+
type: "missing-default-export",
|
|
5973
|
+
message: "Missing default export \u2014 page component must have a default export"
|
|
6183
5974
|
});
|
|
6184
5975
|
}
|
|
6185
|
-
const
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
5976
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
5977
|
+
const beforeImports = /* @__PURE__ */ new Set();
|
|
5978
|
+
const afterImports = /* @__PURE__ */ new Set();
|
|
5979
|
+
for (const match of before.matchAll(importRegex)) {
|
|
5980
|
+
match[1].split(",").forEach((s) => beforeImports.add(s.trim()));
|
|
5981
|
+
}
|
|
5982
|
+
for (const match of after.matchAll(importRegex)) {
|
|
5983
|
+
match[1].split(",").forEach((s) => afterImports.add(s.trim()));
|
|
5984
|
+
}
|
|
5985
|
+
for (const symbol of beforeImports) {
|
|
5986
|
+
if (!afterImports.has(symbol) && symbol.length > 0) {
|
|
5987
|
+
const codeWithoutImports = after.replace(/^import\s+.*$/gm, "");
|
|
5988
|
+
const symbolRegex = new RegExp(`\\b${symbol}\\b`);
|
|
5989
|
+
if (symbolRegex.test(codeWithoutImports)) {
|
|
5990
|
+
issues.push({
|
|
5991
|
+
type: "missing-import",
|
|
5992
|
+
symbol,
|
|
5993
|
+
message: `Import for "${symbol}" was removed but symbol is still used in code`
|
|
5994
|
+
});
|
|
5995
|
+
}
|
|
6195
5996
|
}
|
|
6196
5997
|
}
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
5998
|
+
return issues;
|
|
5999
|
+
}
|
|
6000
|
+
|
|
6001
|
+
// src/commands/chat/utils.ts
|
|
6002
|
+
import { resolve as resolve5 } from "path";
|
|
6003
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
6004
|
+
import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
|
|
6005
|
+
import chalk8 from "chalk";
|
|
6006
|
+
var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
|
|
6007
|
+
function isMarketingRoute(route) {
|
|
6008
|
+
const slug = route.replace(/^\//, "").split("/")[0] || "";
|
|
6009
|
+
return MARKETING_ROUTES.has(slug);
|
|
6010
|
+
}
|
|
6011
|
+
function routeToFsPath(projectRoot, route, isAuth) {
|
|
6012
|
+
const slug = route.replace(/^\//, "");
|
|
6013
|
+
if (isAuth) {
|
|
6014
|
+
return resolve5(projectRoot, "app", "(auth)", slug || "login", "page.tsx");
|
|
6015
|
+
}
|
|
6016
|
+
if (!slug) {
|
|
6017
|
+
return resolve5(projectRoot, "app", "page.tsx");
|
|
6018
|
+
}
|
|
6019
|
+
if (isMarketingRoute(route)) {
|
|
6020
|
+
return resolve5(projectRoot, "app", slug, "page.tsx");
|
|
6021
|
+
}
|
|
6022
|
+
return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
|
|
6023
|
+
}
|
|
6024
|
+
function routeToRelPath(route, isAuth) {
|
|
6025
|
+
const slug = route.replace(/^\//, "");
|
|
6026
|
+
if (isAuth) {
|
|
6027
|
+
return `app/(auth)/${slug || "login"}/page.tsx`;
|
|
6207
6028
|
}
|
|
6208
|
-
if (!
|
|
6209
|
-
|
|
6210
|
-
line: 0,
|
|
6211
|
-
type: "MISSING_LABEL",
|
|
6212
|
-
message: "Inputs found but no Label component \u2014 import Label and add htmlFor on each input",
|
|
6213
|
-
severity: "error"
|
|
6214
|
-
});
|
|
6029
|
+
if (!slug) {
|
|
6030
|
+
return "app/page.tsx";
|
|
6215
6031
|
}
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
issues.push({
|
|
6219
|
-
line: 0,
|
|
6220
|
-
type: "PLACEHOLDER_ONLY_LABEL",
|
|
6221
|
-
message: "Inputs use placeholder only \u2014 add visible Label with htmlFor (placeholder is not a substitute)",
|
|
6222
|
-
severity: "error"
|
|
6223
|
-
});
|
|
6032
|
+
if (isMarketingRoute(route)) {
|
|
6033
|
+
return `app/${slug}/page.tsx`;
|
|
6224
6034
|
}
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6035
|
+
return `app/(app)/${slug}/page.tsx`;
|
|
6036
|
+
}
|
|
6037
|
+
function deduplicatePages(pages) {
|
|
6038
|
+
const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
|
|
6039
|
+
const seen = /* @__PURE__ */ new Map();
|
|
6040
|
+
return pages.filter((page, idx) => {
|
|
6041
|
+
const norm = normalize(page.route);
|
|
6042
|
+
if (seen.has(norm)) return false;
|
|
6043
|
+
seen.set(norm, idx);
|
|
6044
|
+
return true;
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
function extractComponentIdsFromCode(code) {
|
|
6048
|
+
const ids = /* @__PURE__ */ new Set();
|
|
6049
|
+
const allMatches = code.matchAll(/@\/components\/((?:ui\/)?[a-z0-9-]+)/g);
|
|
6050
|
+
for (const m of allMatches) {
|
|
6051
|
+
if (!m[1]) continue;
|
|
6052
|
+
let id = m[1];
|
|
6053
|
+
if (id.startsWith("ui/")) id = id.slice(3);
|
|
6054
|
+
if (id === "shared" || id.startsWith("shared/")) continue;
|
|
6055
|
+
if (id) ids.add(id);
|
|
6235
6056
|
}
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
);
|
|
6240
|
-
if (
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6057
|
+
return ids;
|
|
6058
|
+
}
|
|
6059
|
+
async function warnInlineDuplicates(projectRoot, pageName, pageCode, manifest) {
|
|
6060
|
+
const sectionOrWidget = manifest.shared.filter((e) => e.type === "section" || e.type === "widget");
|
|
6061
|
+
if (sectionOrWidget.length === 0) return;
|
|
6062
|
+
for (const e of sectionOrWidget) {
|
|
6063
|
+
const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
|
|
6064
|
+
const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
|
|
6065
|
+
if (hasImport) continue;
|
|
6066
|
+
const sameNameAsTag = new RegExp(`<\\/?${e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s>]`).test(pageCode);
|
|
6067
|
+
if (sameNameAsTag) {
|
|
6068
|
+
console.log(
|
|
6069
|
+
chalk8.yellow(
|
|
6070
|
+
`
|
|
6071
|
+
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
6072
|
+
)
|
|
6073
|
+
);
|
|
6074
|
+
continue;
|
|
6075
|
+
}
|
|
6076
|
+
try {
|
|
6077
|
+
const fullPath = resolve5(projectRoot, e.file);
|
|
6078
|
+
const sharedSnippet = (await readFile(fullPath)).slice(0, 600);
|
|
6079
|
+
const sharedTokens = new Set(sharedSnippet.match(/\b[a-zA-Z0-9-]{4,}\b/g) ?? []);
|
|
6080
|
+
const pageTokens = pageCode.match(/\b[a-zA-Z0-9-]+\b/g) ?? [];
|
|
6081
|
+
let overlap = 0;
|
|
6082
|
+
for (const t of sharedTokens) {
|
|
6083
|
+
if (pageTokens.includes(t)) overlap++;
|
|
6084
|
+
}
|
|
6085
|
+
if (overlap >= 12 && sharedTokens.size >= 10) {
|
|
6086
|
+
console.log(
|
|
6087
|
+
chalk8.yellow(
|
|
6088
|
+
`
|
|
6089
|
+
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
6090
|
+
)
|
|
6091
|
+
);
|
|
6092
|
+
}
|
|
6093
|
+
} catch {
|
|
6094
|
+
}
|
|
6247
6095
|
}
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
if (
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
severity: "warning"
|
|
6256
|
-
});
|
|
6096
|
+
}
|
|
6097
|
+
async function loadConfig(configPath) {
|
|
6098
|
+
if (!existsSync13(configPath)) {
|
|
6099
|
+
throw new Error(
|
|
6100
|
+
`Design system config not found at ${configPath}
|
|
6101
|
+
Run "coherent init" first to create a project.`
|
|
6102
|
+
);
|
|
6257
6103
|
}
|
|
6258
|
-
const
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6104
|
+
const manager = new DesignSystemManager3(configPath);
|
|
6105
|
+
await manager.load();
|
|
6106
|
+
return manager.getConfig();
|
|
6107
|
+
}
|
|
6108
|
+
function requireProject() {
|
|
6109
|
+
const project = findConfig();
|
|
6110
|
+
if (!project) {
|
|
6111
|
+
exitNotCoherent();
|
|
6266
6112
|
}
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6113
|
+
warnIfVolatile(project.root);
|
|
6114
|
+
return project;
|
|
6115
|
+
}
|
|
6116
|
+
async function resolveTargetFlags(message, options, config2, projectRoot) {
|
|
6117
|
+
if (options.component) {
|
|
6118
|
+
const manifest = await loadManifest4(projectRoot);
|
|
6119
|
+
const target = options.component;
|
|
6120
|
+
const entry = manifest.shared.find(
|
|
6121
|
+
(s) => s.name.toLowerCase() === target.toLowerCase() || s.id.toLowerCase() === target.toLowerCase()
|
|
6122
|
+
);
|
|
6123
|
+
if (entry) {
|
|
6124
|
+
const filePath = resolve5(projectRoot, entry.file);
|
|
6125
|
+
let currentCode = "";
|
|
6126
|
+
if (existsSync13(filePath)) {
|
|
6127
|
+
currentCode = readFileSync8(filePath, "utf-8");
|
|
6128
|
+
}
|
|
6129
|
+
const codeSnippet = currentCode ? `
|
|
6130
|
+
|
|
6131
|
+
Current code of ${entry.name}:
|
|
6132
|
+
\`\`\`tsx
|
|
6133
|
+
${currentCode}
|
|
6134
|
+
\`\`\`` : "";
|
|
6135
|
+
return `Modify the shared component ${entry.name} (${entry.id}, file: ${entry.file}): ${message}. Read the current code below and apply the requested changes. Return the full updated component code as pageCode.${codeSnippet}`;
|
|
6136
|
+
}
|
|
6137
|
+
console.log(chalk8.yellow(`
|
|
6138
|
+
\u26A0\uFE0F Component "${target}" not found in shared components.`));
|
|
6139
|
+
console.log(chalk8.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
|
|
6140
|
+
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
6276
6141
|
}
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6142
|
+
if (options.page) {
|
|
6143
|
+
const target = options.page;
|
|
6144
|
+
const page = config2.pages.find(
|
|
6145
|
+
(p) => p.name.toLowerCase() === target.toLowerCase() || p.id.toLowerCase() === target.toLowerCase() || p.route === target || p.route === "/" + target
|
|
6146
|
+
);
|
|
6147
|
+
if (page) {
|
|
6148
|
+
const relPath = page.route === "/" ? "app/page.tsx" : `app${page.route}/page.tsx`;
|
|
6149
|
+
const filePath = resolve5(projectRoot, relPath);
|
|
6150
|
+
let currentCode = "";
|
|
6151
|
+
if (existsSync13(filePath)) {
|
|
6152
|
+
currentCode = readFileSync8(filePath, "utf-8");
|
|
6153
|
+
}
|
|
6154
|
+
const codeSnippet = currentCode ? `
|
|
6155
|
+
|
|
6156
|
+
Current code of ${page.name} page:
|
|
6157
|
+
\`\`\`tsx
|
|
6158
|
+
${currentCode}
|
|
6159
|
+
\`\`\`` : "";
|
|
6160
|
+
return `Update page "${page.name}" (id: ${page.id}, route: ${page.route}, file: ${relPath}): ${message}. Read the current code below and apply the requested changes.${codeSnippet}`;
|
|
6161
|
+
}
|
|
6162
|
+
console.log(chalk8.yellow(`
|
|
6163
|
+
\u26A0\uFE0F Page "${target}" not found.`));
|
|
6164
|
+
console.log(chalk8.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
|
|
6165
|
+
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
6286
6166
|
}
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6167
|
+
if (options.token) {
|
|
6168
|
+
const target = options.token;
|
|
6169
|
+
return `Change design token "${target}": ${message}. Update the token value in design-system.config.ts and ensure globals.css reflects the change.`;
|
|
6170
|
+
}
|
|
6171
|
+
return message;
|
|
6172
|
+
}
|
|
6173
|
+
|
|
6174
|
+
// src/commands/chat/request-parser.ts
|
|
6175
|
+
var AUTH_FLOW_PATTERNS = {
|
|
6176
|
+
"/login": ["/register", "/forgot-password"],
|
|
6177
|
+
"/signin": ["/register", "/forgot-password"],
|
|
6178
|
+
"/signup": ["/login"],
|
|
6179
|
+
"/register": ["/login"],
|
|
6180
|
+
"/forgot-password": ["/login", "/reset-password"],
|
|
6181
|
+
"/reset-password": ["/login"]
|
|
6182
|
+
};
|
|
6183
|
+
var PAGE_RELATIONSHIP_RULES = [
|
|
6184
|
+
{
|
|
6185
|
+
trigger: /\/(products|catalog|marketplace|listings|shop|store)\b/i,
|
|
6186
|
+
related: [{ id: "product-detail", name: "Product Detail", route: "/products/[id]" }]
|
|
6187
|
+
},
|
|
6188
|
+
{
|
|
6189
|
+
trigger: /\/(blog|news|articles|posts)\b/i,
|
|
6190
|
+
related: [{ id: "article-detail", name: "Article", route: "/blog/[slug]" }]
|
|
6191
|
+
},
|
|
6192
|
+
{
|
|
6193
|
+
trigger: /\/(campaigns|ads|ad-campaigns)\b/i,
|
|
6194
|
+
related: [{ id: "campaign-detail", name: "Campaign Detail", route: "/campaigns/[id]" }]
|
|
6195
|
+
},
|
|
6196
|
+
{
|
|
6197
|
+
trigger: /\/(dashboard|admin)\b/i,
|
|
6198
|
+
related: [{ id: "settings", name: "Settings", route: "/settings" }]
|
|
6199
|
+
},
|
|
6200
|
+
{
|
|
6201
|
+
trigger: /\/pricing\b/i,
|
|
6202
|
+
related: [{ id: "checkout", name: "Checkout", route: "/checkout" }]
|
|
6203
|
+
}
|
|
6204
|
+
];
|
|
6205
|
+
function extractInternalLinks(code) {
|
|
6206
|
+
const links = /* @__PURE__ */ new Set();
|
|
6207
|
+
const hrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
|
|
6208
|
+
let m;
|
|
6209
|
+
while ((m = hrefRe.exec(code)) !== null) {
|
|
6210
|
+
const route = m[1];
|
|
6211
|
+
if (route === "/" || route.startsWith("/design-system") || route.startsWith("/#") || route.startsWith("/api"))
|
|
6212
|
+
continue;
|
|
6213
|
+
links.add(route);
|
|
6296
6214
|
}
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
if (
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6215
|
+
return [...links];
|
|
6216
|
+
}
|
|
6217
|
+
function inferRelatedPages(plannedPages) {
|
|
6218
|
+
const plannedRoutes = new Set(plannedPages.map((p) => p.route));
|
|
6219
|
+
const inferred = [];
|
|
6220
|
+
for (const { route } of plannedPages) {
|
|
6221
|
+
const authRelated = AUTH_FLOW_PATTERNS[route];
|
|
6222
|
+
if (authRelated) {
|
|
6223
|
+
for (const rel of authRelated) {
|
|
6224
|
+
if (!plannedRoutes.has(rel)) {
|
|
6225
|
+
const slug = rel.slice(1);
|
|
6226
|
+
const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
6227
|
+
inferred.push({ id: slug, name, route: rel });
|
|
6228
|
+
plannedRoutes.add(rel);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
}
|
|
6232
|
+
for (const rule of PAGE_RELATIONSHIP_RULES) {
|
|
6233
|
+
if (rule.trigger.test(route)) {
|
|
6234
|
+
for (const rel of rule.related) {
|
|
6235
|
+
if (!plannedRoutes.has(rel.route)) {
|
|
6236
|
+
inferred.push(rel);
|
|
6237
|
+
plannedRoutes.add(rel.route);
|
|
6238
|
+
}
|
|
6315
6239
|
}
|
|
6316
6240
|
}
|
|
6317
6241
|
}
|
|
6318
6242
|
}
|
|
6319
|
-
return
|
|
6243
|
+
return inferred;
|
|
6320
6244
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
const beforeQuoteFix = fixed;
|
|
6325
|
-
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
6326
|
-
if (fixed !== beforeQuoteFix) {
|
|
6327
|
-
fixes.push("fixed escaped closing quotes in strings");
|
|
6328
|
-
}
|
|
6329
|
-
const beforeEntityFix = fixed;
|
|
6330
|
-
fixed = fixed.replace(/<=/g, "<=");
|
|
6331
|
-
fixed = fixed.replace(/>=/g, ">=");
|
|
6332
|
-
fixed = fixed.replace(/&&/g, "&&");
|
|
6333
|
-
fixed = fixed.replace(/([\w)\]])\s*<\s*([\w(])/g, "$1 < $2");
|
|
6334
|
-
fixed = fixed.replace(/([\w)\]])\s*>\s*([\w(])/g, "$1 > $2");
|
|
6335
|
-
if (fixed !== beforeEntityFix) {
|
|
6336
|
-
fixes.push("Fixed syntax issues");
|
|
6337
|
-
}
|
|
6338
|
-
const beforeLtFix = fixed;
|
|
6339
|
-
fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1<$2");
|
|
6340
|
-
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$2");
|
|
6341
|
-
if (fixed !== beforeLtFix) {
|
|
6342
|
-
fixes.push("escaped < in JSX text content");
|
|
6343
|
-
}
|
|
6344
|
-
if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
|
|
6345
|
-
fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
|
|
6346
|
-
fixes.push("text-base \u2192 text-sm");
|
|
6347
|
-
}
|
|
6348
|
-
if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
|
|
6349
|
-
fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
|
|
6350
|
-
fixes.push("large text in CardTitle \u2192 removed");
|
|
6351
|
-
}
|
|
6352
|
-
if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
|
|
6353
|
-
fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
|
|
6354
|
-
fixes.push("heavy shadow \u2192 shadow-sm");
|
|
6355
|
-
}
|
|
6356
|
-
const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
|
|
6357
|
-
const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
|
|
6358
|
-
fixed
|
|
6245
|
+
function impliesFullWebsite(message) {
|
|
6246
|
+
return /\b(create|build|make|design)\b.{0,80}\b(website|web\s*site|web\s*app|application|app|platform|portal|marketplace|site)\b/i.test(
|
|
6247
|
+
message
|
|
6359
6248
|
);
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6249
|
+
}
|
|
6250
|
+
function extractPageNamesFromMessage(message) {
|
|
6251
|
+
const pages = [];
|
|
6252
|
+
const known = {
|
|
6253
|
+
home: "/",
|
|
6254
|
+
landing: "/",
|
|
6255
|
+
dashboard: "/dashboard",
|
|
6256
|
+
about: "/about",
|
|
6257
|
+
"about us": "/about",
|
|
6258
|
+
contact: "/contact",
|
|
6259
|
+
contacts: "/contacts",
|
|
6260
|
+
pricing: "/pricing",
|
|
6261
|
+
settings: "/settings",
|
|
6262
|
+
account: "/account",
|
|
6263
|
+
"personal account": "/account",
|
|
6264
|
+
registration: "/registration",
|
|
6265
|
+
signup: "/signup",
|
|
6266
|
+
"sign up": "/signup",
|
|
6267
|
+
login: "/login",
|
|
6268
|
+
"sign in": "/login",
|
|
6269
|
+
catalogue: "/catalogue",
|
|
6270
|
+
catalog: "/catalog",
|
|
6271
|
+
blog: "/blog",
|
|
6272
|
+
portfolio: "/portfolio",
|
|
6273
|
+
features: "/features",
|
|
6274
|
+
services: "/services",
|
|
6275
|
+
faq: "/faq",
|
|
6276
|
+
team: "/team"
|
|
6277
|
+
};
|
|
6278
|
+
const lower = message.toLowerCase();
|
|
6279
|
+
for (const [key, route] of Object.entries(known)) {
|
|
6280
|
+
if (lower.includes(key)) {
|
|
6281
|
+
const name = key.split(" ").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
6282
|
+
const id = route.slice(1) || "home";
|
|
6283
|
+
if (!pages.some((p) => p.route === route)) {
|
|
6284
|
+
pages.push({ name, id, route });
|
|
6377
6285
|
}
|
|
6378
|
-
const tail = fixed.slice(i);
|
|
6379
|
-
const semi = tail.match(/^\s*;/);
|
|
6380
|
-
const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
|
|
6381
|
-
fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
|
|
6382
|
-
fixes.push('removed metadata export (conflicts with "use client")');
|
|
6383
6286
|
}
|
|
6384
6287
|
}
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6288
|
+
return pages;
|
|
6289
|
+
}
|
|
6290
|
+
function normalizeRequest(request, config2) {
|
|
6291
|
+
const changes = request.changes;
|
|
6292
|
+
const VALID_TYPES = [
|
|
6293
|
+
"update-token",
|
|
6294
|
+
"add-component",
|
|
6295
|
+
"modify-component",
|
|
6296
|
+
"add-layout-block",
|
|
6297
|
+
"modify-layout-block",
|
|
6298
|
+
"add-page",
|
|
6299
|
+
"update-page",
|
|
6300
|
+
"update-navigation",
|
|
6301
|
+
"link-shared",
|
|
6302
|
+
"promote-and-link"
|
|
6303
|
+
];
|
|
6304
|
+
if (!VALID_TYPES.includes(request.type)) {
|
|
6305
|
+
return { error: `Unknown action "${request.type}". Valid: ${VALID_TYPES.join(", ")}` };
|
|
6395
6306
|
}
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6307
|
+
const findPage = (target) => config2.pages.find(
|
|
6308
|
+
(p) => p.id === target || p.route === target || p.name?.toLowerCase() === String(target).toLowerCase()
|
|
6309
|
+
);
|
|
6310
|
+
switch (request.type) {
|
|
6311
|
+
case "update-page": {
|
|
6312
|
+
const page = findPage(request.target);
|
|
6313
|
+
if (!page && changes?.pageCode) {
|
|
6314
|
+
const targetStr = String(request.target);
|
|
6315
|
+
const id = targetStr.replace(/^\//, "") || "home";
|
|
6316
|
+
return {
|
|
6317
|
+
...request,
|
|
6318
|
+
type: "add-page",
|
|
6319
|
+
target: "new",
|
|
6320
|
+
changes: {
|
|
6321
|
+
id,
|
|
6322
|
+
name: changes.name || id.charAt(0).toUpperCase() + id.slice(1) || "Home",
|
|
6323
|
+
route: targetStr.startsWith("/") ? targetStr : `/${targetStr}`,
|
|
6324
|
+
...changes
|
|
6325
|
+
}
|
|
6326
|
+
};
|
|
6327
|
+
}
|
|
6328
|
+
if (!page) {
|
|
6329
|
+
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
6330
|
+
return { error: `Page "${request.target}" not found. Available: ${available || "none"}` };
|
|
6331
|
+
}
|
|
6332
|
+
if (page.id !== request.target) {
|
|
6333
|
+
return { ...request, target: page.id };
|
|
6334
|
+
}
|
|
6335
|
+
break;
|
|
6412
6336
|
}
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
if (
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6337
|
+
case "add-page": {
|
|
6338
|
+
if (!changes) break;
|
|
6339
|
+
let route = changes.route || "";
|
|
6340
|
+
if (route && !route.startsWith("/")) route = `/${route}`;
|
|
6341
|
+
if (route) changes.route = route;
|
|
6342
|
+
const existingByRoute = config2.pages.find((p) => p.route === route);
|
|
6343
|
+
if (existingByRoute && route) {
|
|
6344
|
+
return {
|
|
6345
|
+
...request,
|
|
6346
|
+
type: "update-page",
|
|
6347
|
+
target: existingByRoute.id
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
if (!changes.id && changes.name) {
|
|
6351
|
+
changes.id = String(changes.name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
6352
|
+
}
|
|
6353
|
+
if (!changes.id && route) {
|
|
6354
|
+
changes.id = route.replace(/^\//, "") || "home";
|
|
6355
|
+
}
|
|
6356
|
+
break;
|
|
6357
|
+
}
|
|
6358
|
+
case "modify-component": {
|
|
6359
|
+
const componentId = request.target;
|
|
6360
|
+
const existingComp = config2.components.find((c) => c.id === componentId);
|
|
6361
|
+
if (!existingComp) {
|
|
6362
|
+
return {
|
|
6363
|
+
...request,
|
|
6364
|
+
type: "add-component",
|
|
6365
|
+
target: "new"
|
|
6366
|
+
};
|
|
6422
6367
|
}
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
"bg-gray-900": "bg-background",
|
|
6433
|
-
"bg-zinc-800": "bg-muted",
|
|
6434
|
-
"bg-slate-800": "bg-muted",
|
|
6435
|
-
"bg-gray-800": "bg-muted",
|
|
6436
|
-
"bg-zinc-100": "bg-muted",
|
|
6437
|
-
"bg-slate-100": "bg-muted",
|
|
6438
|
-
"bg-gray-100": "bg-muted",
|
|
6439
|
-
"bg-white": "bg-background",
|
|
6440
|
-
"bg-black": "bg-background",
|
|
6441
|
-
"text-white": "text-foreground",
|
|
6442
|
-
"text-black": "text-foreground",
|
|
6443
|
-
"text-zinc-100": "text-foreground",
|
|
6444
|
-
"text-zinc-200": "text-foreground",
|
|
6445
|
-
"text-slate-100": "text-foreground",
|
|
6446
|
-
"text-gray-100": "text-foreground",
|
|
6447
|
-
"text-zinc-400": "text-muted-foreground",
|
|
6448
|
-
"text-zinc-500": "text-muted-foreground",
|
|
6449
|
-
"text-slate-400": "text-muted-foreground",
|
|
6450
|
-
"text-slate-500": "text-muted-foreground",
|
|
6451
|
-
"text-gray-400": "text-muted-foreground",
|
|
6452
|
-
"text-gray-500": "text-muted-foreground",
|
|
6453
|
-
"border-zinc-700": "border-border",
|
|
6454
|
-
"border-zinc-800": "border-border",
|
|
6455
|
-
"border-slate-700": "border-border",
|
|
6456
|
-
"border-gray-700": "border-border",
|
|
6457
|
-
"border-zinc-200": "border-border",
|
|
6458
|
-
"border-slate-200": "border-border",
|
|
6459
|
-
"border-gray-200": "border-border"
|
|
6460
|
-
};
|
|
6461
|
-
const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
|
|
6462
|
-
const isInsideTerminalBlock = (offset) => {
|
|
6463
|
-
const preceding = fixed.slice(Math.max(0, offset - 600), offset);
|
|
6464
|
-
if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
|
|
6465
|
-
if (!/font-mono/.test(preceding)) return false;
|
|
6466
|
-
const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
|
|
6467
|
-
const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
|
|
6468
|
-
return lastTerminal > lastClose;
|
|
6469
|
-
};
|
|
6470
|
-
let hadColorFix = false;
|
|
6471
|
-
fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
|
|
6472
|
-
if (isCodeContext(classes)) return fullMatch;
|
|
6473
|
-
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
6474
|
-
let result = classes;
|
|
6475
|
-
const accentColorRe = /\b(bg|text|border)-(emerald|blue|violet|indigo|purple|teal|cyan|sky|rose|amber)-(\d+)\b/g;
|
|
6476
|
-
result = result.replace(accentColorRe, (m, prefix, _color, shade) => {
|
|
6477
|
-
if (colorMap[m]) {
|
|
6478
|
-
hadColorFix = true;
|
|
6479
|
-
return colorMap[m];
|
|
6480
|
-
}
|
|
6481
|
-
const n = parseInt(shade);
|
|
6482
|
-
if (prefix === "bg") {
|
|
6483
|
-
if (n >= 500 && n <= 700) {
|
|
6484
|
-
hadColorFix = true;
|
|
6485
|
-
return "bg-primary";
|
|
6486
|
-
}
|
|
6487
|
-
if (n >= 100 && n <= 200) {
|
|
6488
|
-
hadColorFix = true;
|
|
6489
|
-
return "bg-primary/10";
|
|
6368
|
+
if (changes) {
|
|
6369
|
+
if (typeof changes.id === "string" && changes.id !== componentId) {
|
|
6370
|
+
const targetExists = config2.components.some((c) => c.id === changes.id);
|
|
6371
|
+
if (!targetExists) {
|
|
6372
|
+
return { ...request, type: "add-component", target: "new" };
|
|
6373
|
+
}
|
|
6374
|
+
return {
|
|
6375
|
+
error: `Cannot change component "${componentId}" to "${changes.id}" \u2014 "${changes.id}" already exists.`
|
|
6376
|
+
};
|
|
6490
6377
|
}
|
|
6491
|
-
if (
|
|
6492
|
-
|
|
6493
|
-
|
|
6378
|
+
if (typeof changes.name === "string") {
|
|
6379
|
+
const newName = changes.name.toLowerCase();
|
|
6380
|
+
const curName = existingComp.name.toLowerCase();
|
|
6381
|
+
const curId = componentId.toLowerCase();
|
|
6382
|
+
const nameOk = newName === curName || newName === curId || newName.includes(curId) || curId.includes(newName);
|
|
6383
|
+
if (!nameOk) {
|
|
6384
|
+
delete changes.name;
|
|
6385
|
+
}
|
|
6494
6386
|
}
|
|
6495
6387
|
}
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6388
|
+
break;
|
|
6389
|
+
}
|
|
6390
|
+
case "add-component": {
|
|
6391
|
+
if (changes) {
|
|
6392
|
+
const shadcn = changes.shadcnComponent;
|
|
6393
|
+
const id = changes.id;
|
|
6394
|
+
if (shadcn && id && id !== shadcn) {
|
|
6395
|
+
changes.id = shadcn;
|
|
6504
6396
|
}
|
|
6505
6397
|
}
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
if (colorMap[m]) {
|
|
6515
|
-
hadColorFix = true;
|
|
6516
|
-
return colorMap[m];
|
|
6517
|
-
}
|
|
6518
|
-
const n = parseInt(shade);
|
|
6519
|
-
if (prefix === "bg") {
|
|
6520
|
-
if (n >= 800) {
|
|
6521
|
-
hadColorFix = true;
|
|
6522
|
-
return "bg-background";
|
|
6398
|
+
break;
|
|
6399
|
+
}
|
|
6400
|
+
case "link-shared": {
|
|
6401
|
+
if (changes) {
|
|
6402
|
+
const page = findPage(request.target);
|
|
6403
|
+
if (!page) {
|
|
6404
|
+
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
6405
|
+
return { error: `Page "${request.target}" not found for link-shared. Available: ${available || "none"}` };
|
|
6523
6406
|
}
|
|
6524
|
-
if (
|
|
6525
|
-
|
|
6526
|
-
return "bg-muted";
|
|
6407
|
+
if (page.id !== request.target) {
|
|
6408
|
+
return { ...request, target: page.id };
|
|
6527
6409
|
}
|
|
6528
6410
|
}
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
}
|
|
6411
|
+
break;
|
|
6412
|
+
}
|
|
6413
|
+
case "promote-and-link": {
|
|
6414
|
+
const sourcePage = findPage(request.target);
|
|
6415
|
+
if (!sourcePage) {
|
|
6416
|
+
const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
6417
|
+
return {
|
|
6418
|
+
error: `Source page "${request.target}" not found for promote-and-link. Available: ${available || "none"}`
|
|
6419
|
+
};
|
|
6538
6420
|
}
|
|
6539
|
-
if (
|
|
6540
|
-
|
|
6541
|
-
return "border-border";
|
|
6421
|
+
if (sourcePage.id !== request.target) {
|
|
6422
|
+
return { ...request, target: sourcePage.id };
|
|
6542
6423
|
}
|
|
6543
|
-
|
|
6544
|
-
});
|
|
6545
|
-
if (result !== classes) return `className="${result}"`;
|
|
6546
|
-
return fullMatch;
|
|
6547
|
-
});
|
|
6548
|
-
if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
|
|
6549
|
-
const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
6550
|
-
if (lucideImportMatch) {
|
|
6551
|
-
let lucideExports = null;
|
|
6552
|
-
try {
|
|
6553
|
-
const { createRequire } = await import("module");
|
|
6554
|
-
const require2 = createRequire(process.cwd() + "/package.json");
|
|
6555
|
-
const lr = require2("lucide-react");
|
|
6556
|
-
lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
6557
|
-
} catch {
|
|
6424
|
+
break;
|
|
6558
6425
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6426
|
+
}
|
|
6427
|
+
return request;
|
|
6428
|
+
}
|
|
6429
|
+
function applyDefaults(request) {
|
|
6430
|
+
if (request.type === "add-page" && request.changes && typeof request.changes === "object") {
|
|
6431
|
+
const changes = request.changes;
|
|
6432
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6433
|
+
const name = changes.name || "New Page";
|
|
6434
|
+
let id = changes.id || name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
6435
|
+
if (!/^[a-z]/.test(id)) id = `page-${id}`;
|
|
6436
|
+
const route = changes.route || `/${id}`;
|
|
6437
|
+
const hasPageCode = typeof changes.pageCode === "string" && changes.pageCode.trim() !== "";
|
|
6438
|
+
const base = {
|
|
6439
|
+
id,
|
|
6440
|
+
name,
|
|
6441
|
+
route: route.startsWith("/") ? route : `/${route}`,
|
|
6442
|
+
layout: changes.layout || "centered",
|
|
6443
|
+
title: changes.title || name,
|
|
6444
|
+
description: changes.description || `${name} page`,
|
|
6445
|
+
createdAt: changes.createdAt || now,
|
|
6446
|
+
updatedAt: changes.updatedAt || now,
|
|
6447
|
+
requiresAuth: changes.requiresAuth ?? false,
|
|
6448
|
+
noIndex: changes.noIndex ?? false
|
|
6449
|
+
};
|
|
6450
|
+
const sections = Array.isArray(changes.sections) ? changes.sections.map((section, idx) => ({
|
|
6451
|
+
id: section.id || `section-${idx}`,
|
|
6452
|
+
name: section.name || `Section ${idx + 1}`,
|
|
6453
|
+
componentId: section.componentId || "button",
|
|
6454
|
+
order: typeof section.order === "number" ? section.order : idx,
|
|
6455
|
+
props: section.props || {}
|
|
6456
|
+
})) : [];
|
|
6457
|
+
return {
|
|
6458
|
+
...request,
|
|
6459
|
+
changes: {
|
|
6460
|
+
...base,
|
|
6461
|
+
sections,
|
|
6462
|
+
...hasPageCode ? { pageCode: changes.pageCode, generatedWithPageCode: true } : {},
|
|
6463
|
+
...changes.pageType ? { pageType: changes.pageType } : {},
|
|
6464
|
+
...changes.structuredContent ? { structuredContent: changes.structuredContent } : {}
|
|
6563
6465
|
}
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6466
|
+
};
|
|
6467
|
+
}
|
|
6468
|
+
if (request.type === "add-component" && request.changes && typeof request.changes === "object") {
|
|
6469
|
+
const changes = request.changes;
|
|
6470
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6471
|
+
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
6472
|
+
let normalizedVariants = [];
|
|
6473
|
+
if (Array.isArray(changes.variants)) {
|
|
6474
|
+
normalizedVariants = changes.variants.map((v) => {
|
|
6475
|
+
if (typeof v === "string") return { name: v, className: "" };
|
|
6476
|
+
if (v && typeof v === "object" && "name" in v) {
|
|
6477
|
+
return {
|
|
6478
|
+
name: v.name,
|
|
6479
|
+
className: v.className ?? ""
|
|
6480
|
+
};
|
|
6481
|
+
}
|
|
6482
|
+
return { name: "default", className: "" };
|
|
6483
|
+
});
|
|
6484
|
+
}
|
|
6485
|
+
let normalizedSizes = [];
|
|
6486
|
+
if (Array.isArray(changes.sizes)) {
|
|
6487
|
+
normalizedSizes = changes.sizes.map((s) => {
|
|
6488
|
+
if (typeof s === "string") {
|
|
6489
|
+
const name = validSizeNames.includes(s) ? s : "md";
|
|
6490
|
+
return { name, className: "" };
|
|
6491
|
+
}
|
|
6492
|
+
if (s && typeof s === "object" && "name" in s) {
|
|
6493
|
+
const raw = s.name;
|
|
6494
|
+
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
6495
|
+
return { name, className: s.className ?? "" };
|
|
6496
|
+
}
|
|
6497
|
+
return { name: "md", className: "" };
|
|
6498
|
+
});
|
|
6499
|
+
}
|
|
6500
|
+
return {
|
|
6501
|
+
...request,
|
|
6502
|
+
changes: {
|
|
6503
|
+
...changes,
|
|
6504
|
+
variants: normalizedVariants,
|
|
6505
|
+
sizes: normalizedSizes,
|
|
6506
|
+
createdAt: now,
|
|
6507
|
+
updatedAt: now
|
|
6570
6508
|
}
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6509
|
+
};
|
|
6510
|
+
}
|
|
6511
|
+
if (request.type === "modify-component" && request.changes && typeof request.changes === "object") {
|
|
6512
|
+
const changes = request.changes;
|
|
6513
|
+
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
6514
|
+
let normalizedVariants;
|
|
6515
|
+
if (Array.isArray(changes.variants)) {
|
|
6516
|
+
normalizedVariants = changes.variants.map((v) => {
|
|
6517
|
+
if (typeof v === "string") return { name: v, className: "" };
|
|
6518
|
+
if (v && typeof v === "object" && "name" in v) {
|
|
6519
|
+
return {
|
|
6520
|
+
name: v.name,
|
|
6521
|
+
className: v.className ?? ""
|
|
6522
|
+
};
|
|
6578
6523
|
}
|
|
6579
|
-
|
|
6524
|
+
return { name: "default", className: "" };
|
|
6525
|
+
});
|
|
6526
|
+
}
|
|
6527
|
+
let normalizedSizes;
|
|
6528
|
+
if (Array.isArray(changes.sizes)) {
|
|
6529
|
+
normalizedSizes = changes.sizes.map((s) => {
|
|
6530
|
+
if (typeof s === "string") {
|
|
6531
|
+
const name = validSizeNames.includes(s) ? s : "md";
|
|
6532
|
+
return { name, className: "" };
|
|
6533
|
+
}
|
|
6534
|
+
if (s && typeof s === "object" && "name" in s) {
|
|
6535
|
+
const raw = s.name;
|
|
6536
|
+
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
6537
|
+
return { name, className: s.className ?? "" };
|
|
6538
|
+
}
|
|
6539
|
+
return { name: "md", className: "" };
|
|
6540
|
+
});
|
|
6541
|
+
}
|
|
6542
|
+
return {
|
|
6543
|
+
...request,
|
|
6544
|
+
changes: {
|
|
6545
|
+
...changes,
|
|
6546
|
+
...normalizedVariants !== void 0 && { variants: normalizedVariants },
|
|
6547
|
+
...normalizedSizes !== void 0 && { sizes: normalizedSizes }
|
|
6580
6548
|
}
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6549
|
+
};
|
|
6550
|
+
}
|
|
6551
|
+
return request;
|
|
6552
|
+
}
|
|
6553
|
+
|
|
6554
|
+
// src/utils/page-analyzer.ts
|
|
6555
|
+
var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
|
|
6556
|
+
var VISUAL_WORDS = /\b(grid lines?|glow|radial|gradient|blur|shadow|overlay|animation|particles?|dots?|vertical|horizontal|decorat|behind|background|divider|spacer|wrapper|container|inner|outer|absolute|relative|translate|opacity|z-index|transition)\b/i;
|
|
6557
|
+
function analyzePageCode(code) {
|
|
6558
|
+
return {
|
|
6559
|
+
sections: extractSections(code),
|
|
6560
|
+
componentUsage: extractComponentUsage(code),
|
|
6561
|
+
iconCount: extractIconCount(code),
|
|
6562
|
+
layoutPattern: inferLayoutPattern(code),
|
|
6563
|
+
hasForm: detectFormUsage(code),
|
|
6564
|
+
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6565
|
+
};
|
|
6566
|
+
}
|
|
6567
|
+
function extractSections(code) {
|
|
6568
|
+
const sections = [];
|
|
6569
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6570
|
+
const commentRe = /\{\/\*\s*(.+?)\s*\*\/\}/g;
|
|
6571
|
+
let m;
|
|
6572
|
+
while ((m = commentRe.exec(code)) !== null) {
|
|
6573
|
+
const raw = m[1].trim();
|
|
6574
|
+
const name = raw.replace(/[─━—–]+/g, "").replace(/\s*section\s*$/i, "").replace(/^section\s*:\s*/i, "").trim();
|
|
6575
|
+
if (!name || name.length <= 1 || name.length >= 40) continue;
|
|
6576
|
+
if (seen.has(name.toLowerCase())) continue;
|
|
6577
|
+
const wordCount = name.split(/\s+/).length;
|
|
6578
|
+
if (wordCount > 5) continue;
|
|
6579
|
+
if (/[{}()=<>;:`"']/.test(name)) continue;
|
|
6580
|
+
if (/^[a-z]/.test(name) && wordCount > 2) continue;
|
|
6581
|
+
if (VISUAL_WORDS.test(name)) continue;
|
|
6582
|
+
seen.add(name.toLowerCase());
|
|
6583
|
+
sections.push({ name, order: sections.length });
|
|
6584
|
+
}
|
|
6585
|
+
if (sections.length === 0) {
|
|
6586
|
+
const sectionTagRe = /<section[^>]*>[\s\S]*?<h[12][^>]*>\s*([^<]+)/g;
|
|
6587
|
+
while ((m = sectionTagRe.exec(code)) !== null) {
|
|
6588
|
+
const name = m[1].trim();
|
|
6589
|
+
if (name && name.length > 1 && name.length < 40 && !seen.has(name.toLowerCase())) {
|
|
6590
|
+
seen.add(name.toLowerCase());
|
|
6591
|
+
sections.push({ name, order: sections.length });
|
|
6589
6592
|
}
|
|
6590
6593
|
}
|
|
6591
6594
|
}
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6595
|
+
return sections;
|
|
6596
|
+
}
|
|
6597
|
+
function extractComponentUsage(code) {
|
|
6598
|
+
const usage = {};
|
|
6599
|
+
const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
|
|
6600
|
+
const importedComponents = [];
|
|
6601
|
+
let m;
|
|
6602
|
+
while ((m = importRe.exec(code)) !== null) {
|
|
6603
|
+
const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
6604
|
+
importedComponents.push(...names);
|
|
6605
|
+
}
|
|
6606
|
+
for (const comp of importedComponents) {
|
|
6607
|
+
const re = new RegExp(`<${comp}[\\s/>]`, "g");
|
|
6608
|
+
const matches = code.match(re);
|
|
6609
|
+
usage[comp] = matches ? matches.length : 0;
|
|
6610
|
+
}
|
|
6611
|
+
return usage;
|
|
6612
|
+
}
|
|
6613
|
+
function extractIconCount(code) {
|
|
6614
|
+
const m = code.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
|
|
6615
|
+
if (!m) return 0;
|
|
6616
|
+
return m[1].split(",").map((s) => s.trim()).filter(Boolean).length;
|
|
6617
|
+
}
|
|
6618
|
+
function inferLayoutPattern(code) {
|
|
6619
|
+
const funcBodyMatch = code.match(/return\s*\(\s*(<[^]*)/s);
|
|
6620
|
+
const topLevel = funcBodyMatch ? funcBodyMatch[1].slice(0, 500) : code.slice(0, 800);
|
|
6621
|
+
if (/grid-cols|grid\s+md:grid-cols|grid\s+lg:grid-cols/.test(topLevel)) return "grid";
|
|
6622
|
+
if (/sidebar|aside/.test(topLevel)) return "sidebar";
|
|
6623
|
+
if (/max-w-\d|mx-auto|container/.test(topLevel)) return "centered";
|
|
6624
|
+
if (/min-h-screen|min-h-svh/.test(topLevel)) return "full-width";
|
|
6625
|
+
return "unknown";
|
|
6626
|
+
}
|
|
6627
|
+
function detectFormUsage(code) {
|
|
6628
|
+
const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
|
|
6629
|
+
let m;
|
|
6630
|
+
while ((m = importRe.exec(code)) !== null) {
|
|
6631
|
+
const names = m[1].split(",").map((s) => s.trim());
|
|
6632
|
+
if (names.some((n) => FORM_COMPONENTS.has(n))) return true;
|
|
6633
|
+
}
|
|
6634
|
+
return false;
|
|
6635
|
+
}
|
|
6636
|
+
function summarizePageAnalysis(pageName, route, analysis) {
|
|
6637
|
+
const parts = [`${pageName} (${route})`];
|
|
6638
|
+
if (analysis.sections && analysis.sections.length > 0) {
|
|
6639
|
+
parts.push(`sections: ${analysis.sections.map((s) => s.name).join(", ")}`);
|
|
6640
|
+
}
|
|
6641
|
+
if (analysis.componentUsage) {
|
|
6642
|
+
const entries = Object.entries(analysis.componentUsage).filter(([, c]) => c > 0);
|
|
6643
|
+
if (entries.length > 0) {
|
|
6644
|
+
parts.push(`uses: ${entries.map(([n, c]) => `${n}(${c})`).join(", ")}`);
|
|
6601
6645
|
}
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
const baseName = ref.replace(/Icon$/, "");
|
|
6619
|
-
if (lucideExports2.has(ref)) {
|
|
6620
|
-
missing.push(ref);
|
|
6621
|
-
lucideImported.add(ref);
|
|
6622
|
-
} else if (lucideExports2.has(baseName)) {
|
|
6623
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6624
|
-
fixed = fixed.replace(re, baseName);
|
|
6625
|
-
missing.push(baseName);
|
|
6626
|
-
lucideImported.add(baseName);
|
|
6627
|
-
fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
|
|
6628
|
-
} else {
|
|
6629
|
-
const fallback = "Circle";
|
|
6630
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6631
|
-
fixed = fixed.replace(re, fallback);
|
|
6632
|
-
lucideImported.add(fallback);
|
|
6633
|
-
fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
|
-
if (missing.length > 0) {
|
|
6637
|
-
const allNames = [...lucideImported];
|
|
6638
|
-
const origLine = lucideImportMatch2[0];
|
|
6639
|
-
fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
|
|
6640
|
-
fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
|
|
6641
|
-
}
|
|
6646
|
+
}
|
|
6647
|
+
if (analysis.layoutPattern && analysis.layoutPattern !== "unknown") {
|
|
6648
|
+
parts.push(`layout: ${analysis.layoutPattern}`);
|
|
6649
|
+
}
|
|
6650
|
+
if (analysis.hasForm) parts.push("has-form");
|
|
6651
|
+
return `- ${parts.join(". ")}`;
|
|
6652
|
+
}
|
|
6653
|
+
|
|
6654
|
+
// src/utils/concurrency.ts
|
|
6655
|
+
async function pMap(items, fn, concurrency = 3) {
|
|
6656
|
+
const results = new Array(items.length);
|
|
6657
|
+
let nextIndex = 0;
|
|
6658
|
+
async function worker() {
|
|
6659
|
+
while (nextIndex < items.length) {
|
|
6660
|
+
const i = nextIndex++;
|
|
6661
|
+
results[i] = await fn(items[i], i);
|
|
6642
6662
|
}
|
|
6643
6663
|
}
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
|
|
6657
|
-
const height = h || w;
|
|
6658
|
-
return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
|
|
6659
|
-
});
|
|
6660
|
-
fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
|
|
6661
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
6662
|
-
});
|
|
6663
|
-
fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
|
|
6664
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
6664
|
+
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
6665
|
+
await Promise.all(workers);
|
|
6666
|
+
return results;
|
|
6667
|
+
}
|
|
6668
|
+
|
|
6669
|
+
// src/commands/chat/split-generator.ts
|
|
6670
|
+
function buildExistingPagesContext(config2) {
|
|
6671
|
+
const pages = config2.pages || [];
|
|
6672
|
+
const analyzed = pages.filter((p) => p.pageAnalysis);
|
|
6673
|
+
if (analyzed.length === 0) return "";
|
|
6674
|
+
const lines = analyzed.map((p) => {
|
|
6675
|
+
return summarizePageAnalysis(p.name || p.id, p.route, p.pageAnalysis);
|
|
6665
6676
|
});
|
|
6666
|
-
|
|
6667
|
-
|
|
6677
|
+
let ctx = `EXISTING PAGES CONTEXT:
|
|
6678
|
+
${lines.join("\n")}
|
|
6679
|
+
|
|
6680
|
+
Use consistent component choices, spacing, and layout patterns across all pages. Match the style and structure of existing pages.`;
|
|
6681
|
+
const sp = config2.stylePatterns;
|
|
6682
|
+
if (sp && typeof sp === "object") {
|
|
6683
|
+
const parts = [];
|
|
6684
|
+
if (sp.card) parts.push(`Cards: ${sp.card}`);
|
|
6685
|
+
if (sp.section) parts.push(`Sections: ${sp.section}`);
|
|
6686
|
+
if (sp.terminal) parts.push(`Terminal blocks: ${sp.terminal}`);
|
|
6687
|
+
if (sp.iconContainer) parts.push(`Icon containers: ${sp.iconContainer}`);
|
|
6688
|
+
if (sp.heroHeadline) parts.push(`Hero headline: ${sp.heroHeadline}`);
|
|
6689
|
+
if (sp.sectionTitle) parts.push(`Section title: ${sp.sectionTitle}`);
|
|
6690
|
+
if (parts.length > 0) {
|
|
6691
|
+
ctx += `
|
|
6692
|
+
|
|
6693
|
+
PROJECT STYLE PATTERNS (from sync \u2014 match these exactly):
|
|
6694
|
+
${parts.join("\n")}`;
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
6697
|
+
return ctx;
|
|
6698
|
+
}
|
|
6699
|
+
function extractStyleContext(pageCode) {
|
|
6700
|
+
const unique = (arr) => [...new Set(arr)];
|
|
6701
|
+
const cardClasses = (pageCode.match(/className="[^"]*(?:rounded|border|shadow|bg-card)[^"]*"/g) || []).map((m) => m.replace(/className="|"/g, "")).filter((c) => c.includes("rounded") || c.includes("border") || c.includes("card"));
|
|
6702
|
+
const sectionSpacing = unique(pageCode.match(/py-\d+(?:\s+md:py-\d+)?/g) || []);
|
|
6703
|
+
const headingStyles = unique(pageCode.match(/text-(?:\d*xl|lg)\s+font-(?:bold|semibold|medium)/g) || []);
|
|
6704
|
+
const colorPatterns = unique(
|
|
6705
|
+
(pageCode.match(
|
|
6706
|
+
/(?:text|bg|border)-(?:primary|secondary|muted|accent|card|destructive|foreground|background)\S*/g
|
|
6707
|
+
) || []).concat(
|
|
6708
|
+
pageCode.match(
|
|
6709
|
+
/(?:text|bg|border)-(?:emerald|blue|violet|rose|amber|zinc|slate|gray|green|red|orange|indigo|purple|teal|cyan)\S*/g
|
|
6710
|
+
) || []
|
|
6711
|
+
)
|
|
6712
|
+
);
|
|
6713
|
+
const iconPatterns = unique(pageCode.match(/(?:rounded-\S+\s+)?p-\d+(?:\.\d+)?\s*(?:bg-\S+)?/g) || []).filter(
|
|
6714
|
+
(p) => p.includes("bg-") || p.includes("rounded")
|
|
6715
|
+
);
|
|
6716
|
+
const buttonPatterns = unique(
|
|
6717
|
+
(pageCode.match(/className="[^"]*(?:hover:|active:)[^"]*"/g) || []).map((m) => m.replace(/className="|"/g, "")).filter((c) => c.includes("px-") || c.includes("py-") || c.includes("rounded"))
|
|
6718
|
+
);
|
|
6719
|
+
const bgPatterns = unique(pageCode.match(/bg-(?:muted|card|background|zinc|slate|gray)\S*/g) || []);
|
|
6720
|
+
const gapPatterns = unique(pageCode.match(/gap-\d+/g) || []);
|
|
6721
|
+
const gridPatterns = unique(pageCode.match(/grid-cols-\d+|md:grid-cols-\d+|lg:grid-cols-\d+/g) || []);
|
|
6722
|
+
const containerPatterns = unique(pageCode.match(/container\s+max-w-\S+|max-w-\d+xl\s+mx-auto/g) || []);
|
|
6723
|
+
const lines = [];
|
|
6724
|
+
if (containerPatterns.length > 0) {
|
|
6725
|
+
lines.push(`Container (MUST match for alignment with header/footer): ${containerPatterns[0]} px-4`);
|
|
6726
|
+
}
|
|
6727
|
+
if (cardClasses.length > 0) lines.push(`Cards: ${unique(cardClasses).slice(0, 4).join(" | ")}`);
|
|
6728
|
+
if (sectionSpacing.length > 0) lines.push(`Section spacing: ${sectionSpacing.join(", ")}`);
|
|
6729
|
+
if (headingStyles.length > 0) lines.push(`Headings: ${headingStyles.join(", ")}`);
|
|
6730
|
+
if (colorPatterns.length > 0) lines.push(`Colors: ${colorPatterns.slice(0, 15).join(", ")}`);
|
|
6731
|
+
if (iconPatterns.length > 0) lines.push(`Icon containers: ${iconPatterns.slice(0, 4).join(" | ")}`);
|
|
6732
|
+
if (buttonPatterns.length > 0) lines.push(`Buttons: ${buttonPatterns.slice(0, 3).join(" | ")}`);
|
|
6733
|
+
if (bgPatterns.length > 0) lines.push(`Section backgrounds: ${bgPatterns.slice(0, 6).join(", ")}`);
|
|
6734
|
+
if (gapPatterns.length > 0) lines.push(`Gaps: ${gapPatterns.join(", ")}`);
|
|
6735
|
+
if (gridPatterns.length > 0) lines.push(`Grids: ${gridPatterns.join(", ")}`);
|
|
6736
|
+
if (lines.length === 0) return "";
|
|
6737
|
+
return `STYLE CONTEXT (match these patterns exactly for visual consistency with the Home page):
|
|
6738
|
+
${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
6739
|
+
}
|
|
6740
|
+
var VALID_NAV_TYPES = /* @__PURE__ */ new Set(["header", "sidebar", "both"]);
|
|
6741
|
+
function parseNavTypeFromPlan(planResult) {
|
|
6742
|
+
const nav = planResult.navigation;
|
|
6743
|
+
if (nav && typeof nav.type === "string" && VALID_NAV_TYPES.has(nav.type)) {
|
|
6744
|
+
return nav.type;
|
|
6668
6745
|
}
|
|
6669
|
-
return
|
|
6746
|
+
return "header";
|
|
6670
6747
|
}
|
|
6671
|
-
function
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6748
|
+
async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
|
|
6749
|
+
let pageNames = [];
|
|
6750
|
+
spinner.start("Phase 1/4 \u2014 Planning pages...");
|
|
6751
|
+
try {
|
|
6752
|
+
const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
|
|
6753
|
+
const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
|
|
6754
|
+
pageNames = pageReqs.map((r) => {
|
|
6755
|
+
const c = r.changes;
|
|
6756
|
+
const name = c.name || c.id || "page";
|
|
6757
|
+
const id = c.id || name.toLowerCase().replace(/\s+/g, "-");
|
|
6758
|
+
const route = c.route || `/${id}`;
|
|
6759
|
+
return { name, id, route };
|
|
6760
|
+
});
|
|
6761
|
+
const detectedNavType = parseNavTypeFromPlan(planResult);
|
|
6762
|
+
if (detectedNavType !== "header" && modCtx.config.navigation) {
|
|
6763
|
+
modCtx.config.navigation.type = detectedNavType;
|
|
6681
6764
|
}
|
|
6765
|
+
} catch {
|
|
6766
|
+
spinner.text = "AI plan failed \u2014 extracting pages from your request...";
|
|
6682
6767
|
}
|
|
6683
|
-
if (
|
|
6684
|
-
|
|
6685
|
-
for (const w of warnings) {
|
|
6686
|
-
lines.push(` L${w.line}: [${w.type}] ${w.message}`);
|
|
6687
|
-
}
|
|
6768
|
+
if (pageNames.length === 0) {
|
|
6769
|
+
pageNames = extractPageNamesFromMessage(message);
|
|
6688
6770
|
}
|
|
6689
|
-
if (
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
lines.push(` L${i.line}: [${i.type}] ${i.message}`);
|
|
6693
|
-
}
|
|
6771
|
+
if (pageNames.length === 0) {
|
|
6772
|
+
spinner.fail("Could not determine pages to create");
|
|
6773
|
+
return [];
|
|
6694
6774
|
}
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
}
|
|
6775
|
+
pageNames = deduplicatePages(pageNames);
|
|
6776
|
+
const hasHomePage = pageNames.some((p) => p.route === "/");
|
|
6777
|
+
if (!hasHomePage) {
|
|
6778
|
+
const userPages = (modCtx.config.pages || []).filter(
|
|
6779
|
+
(p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
|
|
6780
|
+
);
|
|
6781
|
+
const isFreshProject = userPages.length === 0;
|
|
6782
|
+
if (isFreshProject || impliesFullWebsite(message)) {
|
|
6783
|
+
pageNames.unshift({ name: "Home", id: "home", route: "/" });
|
|
6784
|
+
}
|
|
6705
6785
|
}
|
|
6706
|
-
const
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
});
|
|
6786
|
+
const existingRoutes = new Set((modCtx.config.pages || []).map((p) => p.route).filter(Boolean));
|
|
6787
|
+
const inferred = inferRelatedPages(pageNames).filter((p) => !existingRoutes.has(p.route));
|
|
6788
|
+
if (inferred.length > 0) {
|
|
6789
|
+
pageNames.push(...inferred);
|
|
6790
|
+
pageNames = deduplicatePages(pageNames);
|
|
6712
6791
|
}
|
|
6713
|
-
|
|
6714
|
-
}
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
const
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6792
|
+
const allRoutes = pageNames.map((p) => p.route).join(", ");
|
|
6793
|
+
const allPagesList = pageNames.map((p) => `${p.name} (${p.route})`).join(", ");
|
|
6794
|
+
const inferredNote = inferred.length > 0 ? ` (${inferred.length} auto-inferred)` : "";
|
|
6795
|
+
spinner.succeed(`Phase 1/4 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
|
|
6796
|
+
const homeIdx = pageNames.findIndex((p) => p.route === "/");
|
|
6797
|
+
const homePage = homeIdx !== -1 ? pageNames[homeIdx] : pageNames[0];
|
|
6798
|
+
const remainingPages = pageNames.filter((_, i) => i !== (homeIdx !== -1 ? homeIdx : 0));
|
|
6799
|
+
spinner.start(`Phase 2/4 \u2014 Generating ${homePage.name} page (sets design direction)...`);
|
|
6800
|
+
let homeRequest = null;
|
|
6801
|
+
let homePageCode = "";
|
|
6802
|
+
try {
|
|
6803
|
+
const homeResult = await parseModification(
|
|
6804
|
+
`Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This REPLACES the default placeholder page \u2014 generate a complete, content-rich landing page for the project described above. Generate complete pageCode. Include a branded site-wide <header> with navigation links to ALL these pages: ${allPagesList}. Use these EXACT routes in navigation: ${allRoutes}. Include a <footer> at the bottom. Make it visually polished \u2014 this page sets the design direction for the entire site. Do not generate other pages.`,
|
|
6805
|
+
modCtx,
|
|
6806
|
+
provider,
|
|
6807
|
+
parseOpts
|
|
6808
|
+
);
|
|
6809
|
+
const codePage = homeResult.requests.find((r) => r.type === "add-page");
|
|
6810
|
+
if (codePage) {
|
|
6811
|
+
homeRequest = codePage;
|
|
6812
|
+
homePageCode = codePage.changes?.pageCode || "";
|
|
6813
|
+
}
|
|
6814
|
+
} catch {
|
|
6723
6815
|
}
|
|
6724
|
-
if (!
|
|
6725
|
-
|
|
6726
|
-
type: "
|
|
6727
|
-
|
|
6728
|
-
|
|
6816
|
+
if (!homeRequest) {
|
|
6817
|
+
homeRequest = {
|
|
6818
|
+
type: "add-page",
|
|
6819
|
+
target: "new",
|
|
6820
|
+
changes: { id: homePage.id, name: homePage.name, route: homePage.route }
|
|
6821
|
+
};
|
|
6729
6822
|
}
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
const
|
|
6733
|
-
|
|
6734
|
-
|
|
6823
|
+
spinner.succeed(`Phase 2/4 \u2014 ${homePage.name} page generated`);
|
|
6824
|
+
spinner.start("Phase 3/4 \u2014 Extracting design patterns...");
|
|
6825
|
+
const styleContext = homePageCode ? extractStyleContext(homePageCode) : "";
|
|
6826
|
+
if (styleContext) {
|
|
6827
|
+
const lineCount = styleContext.split("\n").length - 1;
|
|
6828
|
+
spinner.succeed(`Phase 3/4 \u2014 Extracted ${lineCount} style patterns from ${homePage.name}`);
|
|
6829
|
+
} else {
|
|
6830
|
+
spinner.succeed("Phase 3/4 \u2014 No style patterns extracted (Home page had no code)");
|
|
6735
6831
|
}
|
|
6736
|
-
|
|
6737
|
-
|
|
6832
|
+
if (remainingPages.length === 0) {
|
|
6833
|
+
return [homeRequest];
|
|
6738
6834
|
}
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6835
|
+
spinner.start(`Phase 4/4 \u2014 Generating ${remainingPages.length} pages in parallel...`);
|
|
6836
|
+
const sharedNote = "Header and Footer are shared components rendered by the root layout. Do NOT include any site-wide <header>, <nav>, or <footer> in this page. Start with the main content directly.";
|
|
6837
|
+
const routeNote = `EXISTING ROUTES in this project: ${allRoutes}. All internal links MUST point to one of these routes. If a target doesn't exist, use href="#".`;
|
|
6838
|
+
const alignmentNote = 'CRITICAL LAYOUT RULE: Every <section> must wrap its content in a container div matching the header width. Use the EXACT same container classes as shown in the style context (e.g. className="container max-w-6xl px-4" or className="max-w-6xl mx-auto px-4"). Inner content can use narrower max-w for text centering, but the outer section container MUST match.';
|
|
6839
|
+
const existingPagesContext = buildExistingPagesContext(modCtx.config);
|
|
6840
|
+
const AI_CONCURRENCY = 3;
|
|
6841
|
+
let phase4Done = 0;
|
|
6842
|
+
const remainingRequests = await pMap(
|
|
6843
|
+
remainingPages,
|
|
6844
|
+
async ({ name, id, route }) => {
|
|
6845
|
+
const prompt = [
|
|
6846
|
+
`Create ONE page called "${name}" at route "${route}".`,
|
|
6847
|
+
`Context: ${message}.`,
|
|
6848
|
+
`Generate complete pageCode for this single page only. Do not generate other pages.`,
|
|
6849
|
+
sharedNote,
|
|
6850
|
+
routeNote,
|
|
6851
|
+
alignmentNote,
|
|
6852
|
+
existingPagesContext,
|
|
6853
|
+
styleContext
|
|
6854
|
+
].filter(Boolean).join("\n\n");
|
|
6855
|
+
try {
|
|
6856
|
+
const result = await parseModification(prompt, modCtx, provider, parseOpts);
|
|
6857
|
+
phase4Done++;
|
|
6858
|
+
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
6859
|
+
const codePage = result.requests.find((r) => r.type === "add-page");
|
|
6860
|
+
return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
|
|
6861
|
+
} catch {
|
|
6862
|
+
phase4Done++;
|
|
6863
|
+
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
6864
|
+
return { type: "add-page", target: "new", changes: { id, name, route } };
|
|
6865
|
+
}
|
|
6866
|
+
},
|
|
6867
|
+
AI_CONCURRENCY
|
|
6868
|
+
);
|
|
6869
|
+
const allRequests = [homeRequest, ...remainingRequests];
|
|
6870
|
+
const emptyPages = allRequests.filter((r) => r.type === "add-page" && !r.changes?.pageCode);
|
|
6871
|
+
if (emptyPages.length > 0 && emptyPages.length <= 5) {
|
|
6872
|
+
spinner.text = `Retrying ${emptyPages.length} page(s) without code...`;
|
|
6873
|
+
for (const req of emptyPages) {
|
|
6874
|
+
const page = req.changes;
|
|
6875
|
+
const pageName = page.name || page.id || "page";
|
|
6876
|
+
const pageRoute = page.route || `/${pageName.toLowerCase()}`;
|
|
6877
|
+
try {
|
|
6878
|
+
const retryResult = await parseModification(
|
|
6879
|
+
`Create ONE page called "${pageName}" at route "${pageRoute}". Context: ${message}. Generate complete pageCode for this single page only.`,
|
|
6880
|
+
modCtx,
|
|
6881
|
+
provider,
|
|
6882
|
+
parseOpts
|
|
6883
|
+
);
|
|
6884
|
+
const codePage = retryResult.requests.find((r) => r.type === "add-page");
|
|
6885
|
+
if (codePage && codePage.changes?.pageCode) {
|
|
6886
|
+
const idx = allRequests.indexOf(req);
|
|
6887
|
+
if (idx !== -1) allRequests[idx] = codePage;
|
|
6888
|
+
}
|
|
6889
|
+
} catch {
|
|
6749
6890
|
}
|
|
6750
6891
|
}
|
|
6751
6892
|
}
|
|
6752
|
-
|
|
6893
|
+
const withCode = allRequests.filter((r) => r.changes?.pageCode).length;
|
|
6894
|
+
spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
|
|
6895
|
+
return allRequests;
|
|
6753
6896
|
}
|
|
6754
6897
|
|
|
6898
|
+
// src/commands/chat/modification-handler.ts
|
|
6899
|
+
import { resolve as resolve7 } from "path";
|
|
6900
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
6901
|
+
import { dirname as dirname6 } from "path";
|
|
6902
|
+
import chalk11 from "chalk";
|
|
6903
|
+
import {
|
|
6904
|
+
getTemplateForPageType,
|
|
6905
|
+
loadManifest as loadManifest5,
|
|
6906
|
+
saveManifest,
|
|
6907
|
+
updateUsedIn,
|
|
6908
|
+
findSharedComponentByIdOrName,
|
|
6909
|
+
generateSharedComponent as generateSharedComponent3
|
|
6910
|
+
} from "@getcoherent/core";
|
|
6911
|
+
|
|
6755
6912
|
// src/commands/chat/code-generator.ts
|
|
6756
6913
|
import { resolve as resolve6 } from "path";
|
|
6757
6914
|
import { existsSync as existsSync14 } from "fs";
|
|
@@ -7726,10 +7883,13 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7726
7883
|
let codeToWrite = fixedCode;
|
|
7727
7884
|
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
|
|
7728
7885
|
codeToWrite = autoFixed;
|
|
7729
|
-
const
|
|
7730
|
-
if (
|
|
7731
|
-
|
|
7732
|
-
|
|
7886
|
+
const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
|
|
7887
|
+
if (!hasDashboardPage) {
|
|
7888
|
+
const { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
|
|
7889
|
+
if (spaWasFixed) {
|
|
7890
|
+
codeToWrite = spaFixed;
|
|
7891
|
+
autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
|
|
7892
|
+
}
|
|
7733
7893
|
}
|
|
7734
7894
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7735
7895
|
codeToWrite = layoutStripped;
|
|
@@ -7769,10 +7929,9 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7769
7929
|
layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout"),
|
|
7770
7930
|
allShared: manifestForAudit.shared
|
|
7771
7931
|
});
|
|
7772
|
-
|
|
7773
|
-
const issues = validatePageQuality(codeToWrite, validRoutes);
|
|
7932
|
+
let issues = validatePageQuality(codeToWrite);
|
|
7774
7933
|
const errors = issues.filter((i) => i.severity === "error");
|
|
7775
|
-
if (errors.length >=
|
|
7934
|
+
if (errors.length >= 2 && aiProvider) {
|
|
7776
7935
|
console.log(
|
|
7777
7936
|
chalk11.yellow(`
|
|
7778
7937
|
\u{1F504} ${errors.length} quality errors \u2014 attempting AI fix for ${page.name || page.id}...`)
|
|
@@ -7791,12 +7950,19 @@ Rules:
|
|
|
7791
7950
|
- Keep all existing functionality and layout intact`;
|
|
7792
7951
|
const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
|
|
7793
7952
|
if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
|
|
7794
|
-
const recheck = validatePageQuality(fixedCode2
|
|
7953
|
+
const recheck = validatePageQuality(fixedCode2);
|
|
7795
7954
|
const recheckErrors = recheck.filter((i) => i.severity === "error");
|
|
7796
7955
|
if (recheckErrors.length < errors.length) {
|
|
7797
7956
|
codeToWrite = fixedCode2;
|
|
7957
|
+
const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite);
|
|
7958
|
+
if (reFixes.length > 0) {
|
|
7959
|
+
codeToWrite = reFixed;
|
|
7960
|
+
postFixes.push(...reFixes);
|
|
7961
|
+
}
|
|
7798
7962
|
await writeFile(filePath, codeToWrite);
|
|
7799
|
-
|
|
7963
|
+
issues = validatePageQuality(codeToWrite);
|
|
7964
|
+
const finalErrors = issues.filter((i) => i.severity === "error").length;
|
|
7965
|
+
console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
|
|
7800
7966
|
}
|
|
7801
7967
|
}
|
|
7802
7968
|
}
|
|
@@ -7917,10 +8083,13 @@ ${pagesCtx}`
|
|
|
7917
8083
|
let codeToWrite = fixedCode;
|
|
7918
8084
|
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
|
|
7919
8085
|
codeToWrite = autoFixed;
|
|
7920
|
-
const
|
|
7921
|
-
if (
|
|
7922
|
-
|
|
7923
|
-
|
|
8086
|
+
const hasDashboardPage = dsm.getConfig().pages.some((p) => p.route === "/dashboard");
|
|
8087
|
+
if (!hasDashboardPage) {
|
|
8088
|
+
const { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
|
|
8089
|
+
if (spaWasFixed) {
|
|
8090
|
+
codeToWrite = spaFixed;
|
|
8091
|
+
autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
|
|
8092
|
+
}
|
|
7924
8093
|
}
|
|
7925
8094
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7926
8095
|
codeToWrite = layoutStripped;
|
|
@@ -8479,18 +8648,18 @@ async function chatCommand(message, options) {
|
|
|
8479
8648
|
);
|
|
8480
8649
|
const preflightInstalledIds = [];
|
|
8481
8650
|
const allNpmImportsFromPages = /* @__PURE__ */ new Set();
|
|
8651
|
+
const allNeededComponentIds = /* @__PURE__ */ new Set();
|
|
8482
8652
|
for (const pageRequest of pageRequests) {
|
|
8483
8653
|
const page = pageRequest.changes;
|
|
8484
|
-
const neededComponentIds = /* @__PURE__ */ new Set();
|
|
8485
8654
|
page.sections?.forEach(
|
|
8486
8655
|
(section) => {
|
|
8487
8656
|
if (section.componentId) {
|
|
8488
|
-
|
|
8657
|
+
allNeededComponentIds.add(section.componentId);
|
|
8489
8658
|
}
|
|
8490
8659
|
if (section.props?.fields && Array.isArray(section.props.fields)) {
|
|
8491
8660
|
section.props.fields.forEach((field) => {
|
|
8492
8661
|
if (field.component) {
|
|
8493
|
-
|
|
8662
|
+
allNeededComponentIds.add(field.component);
|
|
8494
8663
|
}
|
|
8495
8664
|
});
|
|
8496
8665
|
}
|
|
@@ -8499,7 +8668,7 @@ async function chatCommand(message, options) {
|
|
|
8499
8668
|
if (typeof page.pageCode === "string" && page.pageCode.trim() !== "") {
|
|
8500
8669
|
const importMatches = page.pageCode.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
|
|
8501
8670
|
for (const m of importMatches) {
|
|
8502
|
-
if (m[1])
|
|
8671
|
+
if (m[1]) allNeededComponentIds.add(m[1]);
|
|
8503
8672
|
}
|
|
8504
8673
|
extractNpmPackagesFromCode(page.pageCode).forEach((p) => allNpmImportsFromPages.add(p));
|
|
8505
8674
|
}
|
|
@@ -8514,7 +8683,7 @@ async function chatCommand(message, options) {
|
|
|
8514
8683
|
});
|
|
8515
8684
|
const tmplImports = preview.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
|
|
8516
8685
|
for (const m of tmplImports) {
|
|
8517
|
-
if (m[1])
|
|
8686
|
+
if (m[1]) allNeededComponentIds.add(m[1]);
|
|
8518
8687
|
}
|
|
8519
8688
|
extractNpmPackagesFromCode(preview).forEach((p) => allNpmImportsFromPages.add(p));
|
|
8520
8689
|
} catch {
|
|
@@ -8522,8 +8691,8 @@ async function chatCommand(message, options) {
|
|
|
8522
8691
|
}
|
|
8523
8692
|
}
|
|
8524
8693
|
if (DEBUG4) {
|
|
8525
|
-
console.log(chalk13.gray(
|
|
8526
|
-
|
|
8694
|
+
console.log(chalk13.gray(`
|
|
8695
|
+
[DEBUG] Pre-flight analysis for page "${page.name || page.route}": `));
|
|
8527
8696
|
console.log(chalk13.gray(` Page sections: ${page.sections?.length || 0}`));
|
|
8528
8697
|
if (page.sections?.[0]?.props?.fields) {
|
|
8529
8698
|
console.log(chalk13.gray(` First section has ${page.sections[0].props.fields.length} fields`));
|
|
@@ -8531,64 +8700,67 @@ async function chatCommand(message, options) {
|
|
|
8531
8700
|
console.log(chalk13.gray(` Field ${i}: component=${f.component}`));
|
|
8532
8701
|
});
|
|
8533
8702
|
}
|
|
8534
|
-
console.log("");
|
|
8535
8703
|
}
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8704
|
+
}
|
|
8705
|
+
const INVALID_COMPONENT_IDS = /* @__PURE__ */ new Set(["ui", "shared", "lib", "utils", "hooks", "app", "components"]);
|
|
8706
|
+
for (const id of INVALID_COMPONENT_IDS) allNeededComponentIds.delete(id);
|
|
8707
|
+
if (DEBUG4) {
|
|
8708
|
+
console.log(chalk13.gray("\n[DEBUG] Pre-flight analysis (consolidated):"));
|
|
8709
|
+
console.log(chalk13.gray(` All needed components: ${Array.from(allNeededComponentIds).join(", ")}`));
|
|
8710
|
+
console.log("");
|
|
8711
|
+
}
|
|
8712
|
+
const missingComponents = [];
|
|
8713
|
+
for (const componentId of allNeededComponentIds) {
|
|
8714
|
+
const exists = cm.read(componentId);
|
|
8715
|
+
if (DEBUG4) console.log(chalk13.gray(` Checking ${componentId}: ${exists ? "EXISTS" : "MISSING"}`));
|
|
8716
|
+
if (!exists) {
|
|
8717
|
+
missingComponents.push(componentId);
|
|
8545
8718
|
}
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
}
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
}
|
|
8569
|
-
if (result.success) {
|
|
8570
|
-
preflightInstalledIds.push(shadcnDef.id);
|
|
8571
|
-
console.log(chalk13.green(` \u2728 Auto-installed ${shadcnDef.name} component`));
|
|
8572
|
-
const updatedConfig2 = result.config;
|
|
8573
|
-
dsm.updateConfig(updatedConfig2);
|
|
8574
|
-
cm.updateConfig(updatedConfig2);
|
|
8575
|
-
pm.updateConfig(updatedConfig2);
|
|
8576
|
-
}
|
|
8719
|
+
}
|
|
8720
|
+
if (missingComponents.length > 0) {
|
|
8721
|
+
spinner.stop();
|
|
8722
|
+
console.log(chalk13.cyan("\n\u{1F50D} Pre-flight check: Installing missing components...\n"));
|
|
8723
|
+
for (const componentId of missingComponents) {
|
|
8724
|
+
if (DEBUG4) {
|
|
8725
|
+
console.log(chalk13.gray(` [DEBUG] Trying to install: ${componentId}`));
|
|
8726
|
+
console.log(chalk13.gray(` [DEBUG] isShadcnComponent(${componentId}): ${isShadcnComponent(componentId)}`));
|
|
8727
|
+
}
|
|
8728
|
+
if (isShadcnComponent(componentId)) {
|
|
8729
|
+
try {
|
|
8730
|
+
const shadcnDef = await installShadcnComponent(componentId, projectRoot);
|
|
8731
|
+
if (DEBUG4) console.log(chalk13.gray(` [DEBUG] shadcnDef for ${componentId}: ${shadcnDef ? "OK" : "NULL"}`));
|
|
8732
|
+
if (shadcnDef) {
|
|
8733
|
+
if (DEBUG4) console.log(chalk13.gray(` [DEBUG] Registering ${shadcnDef.id} (${shadcnDef.name})`));
|
|
8734
|
+
const result = await cm.register(shadcnDef);
|
|
8735
|
+
if (DEBUG4) {
|
|
8736
|
+
console.log(
|
|
8737
|
+
chalk13.gray(
|
|
8738
|
+
` [DEBUG] Register result: ${result.success ? "SUCCESS" : "FAILED"}${!result.success && result.message ? ` - ${result.message}` : ""}`
|
|
8739
|
+
)
|
|
8740
|
+
);
|
|
8577
8741
|
}
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8742
|
+
if (result.success) {
|
|
8743
|
+
preflightInstalledIds.push(shadcnDef.id);
|
|
8744
|
+
console.log(chalk13.green(` \u2728 Auto-installed ${shadcnDef.name} component`));
|
|
8745
|
+
const updatedConfig2 = result.config;
|
|
8746
|
+
dsm.updateConfig(updatedConfig2);
|
|
8747
|
+
cm.updateConfig(updatedConfig2);
|
|
8748
|
+
pm.updateConfig(updatedConfig2);
|
|
8583
8749
|
}
|
|
8584
8750
|
}
|
|
8585
|
-
}
|
|
8586
|
-
console.log(chalk13.
|
|
8751
|
+
} catch (error) {
|
|
8752
|
+
console.log(chalk13.red(` \u274C Failed to install ${componentId}:`));
|
|
8753
|
+
console.log(chalk13.red(` ${error instanceof Error ? error.message : error}`));
|
|
8754
|
+
if (error instanceof Error && error.stack) {
|
|
8755
|
+
console.log(chalk13.gray(` ${error.stack.split("\n")[1]}`));
|
|
8756
|
+
}
|
|
8587
8757
|
}
|
|
8758
|
+
} else {
|
|
8759
|
+
console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
|
|
8588
8760
|
}
|
|
8589
|
-
console.log("");
|
|
8590
|
-
spinner.start("Applying modifications...");
|
|
8591
8761
|
}
|
|
8762
|
+
console.log("");
|
|
8763
|
+
spinner.start("Applying modifications...");
|
|
8592
8764
|
}
|
|
8593
8765
|
const installedPkgs = getInstalledPackages(projectRoot);
|
|
8594
8766
|
const neededPkgs = /* @__PURE__ */ new Set([...COHERENT_REQUIRED_PACKAGES, ...allNpmImportsFromPages]);
|
|
@@ -8750,6 +8922,34 @@ async function chatCommand(message, options) {
|
|
|
8750
8922
|
spinner.start("Finalizing...");
|
|
8751
8923
|
}
|
|
8752
8924
|
}
|
|
8925
|
+
const finalConfig = dsm.getConfig();
|
|
8926
|
+
const allRoutes = finalConfig.pages.map((p) => p.route).filter(Boolean);
|
|
8927
|
+
if (allRoutes.length > 1) {
|
|
8928
|
+
const linkIssues = [];
|
|
8929
|
+
for (const result of results) {
|
|
8930
|
+
if (!result.success) continue;
|
|
8931
|
+
for (const mod of result.modified) {
|
|
8932
|
+
if (mod.startsWith("app/") && mod.endsWith("/page.tsx")) {
|
|
8933
|
+
try {
|
|
8934
|
+
const code = readFileSync10(resolve9(projectRoot, mod), "utf-8");
|
|
8935
|
+
const issues = validatePageQuality(code, allRoutes).filter(
|
|
8936
|
+
(i) => i.type === "BROKEN_INTERNAL_LINK"
|
|
8937
|
+
);
|
|
8938
|
+
for (const issue of issues) {
|
|
8939
|
+
linkIssues.push({ page: mod, message: issue.message });
|
|
8940
|
+
}
|
|
8941
|
+
} catch {
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
}
|
|
8945
|
+
}
|
|
8946
|
+
if (linkIssues.length > 0) {
|
|
8947
|
+
console.log(chalk13.yellow("\n\u{1F517} Broken internal links:"));
|
|
8948
|
+
for (const { page, message: message2 } of linkIssues) {
|
|
8949
|
+
console.log(chalk13.dim(` ${page}: ${message2}`));
|
|
8950
|
+
}
|
|
8951
|
+
}
|
|
8952
|
+
}
|
|
8753
8953
|
const updatedConfig = dsm.getConfig();
|
|
8754
8954
|
const darkMatch = /\bdark\s*(theme|mode|background)\b/i.test(message);
|
|
8755
8955
|
const lightMatch = /\blight\s*(theme|mode|background)\b/i.test(message);
|