@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.
Files changed (3) hide show
  1. package/dist/index.js +1867 -1667
  2. package/package.json +10 -10
  3. 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
- return { requests: requestsArray2, uxRecommendations: void 0 };
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 "/") should be a simple redirect using next/navigation redirect('/dashboard') \u2014 OR a standalone landing page. NEVER a multi-view SPA.
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/commands/chat/utils.ts
5046
- import { resolve as resolve5 } from "path";
5047
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
5048
- import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
5049
- import chalk8 from "chalk";
5050
- var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
5051
- function isMarketingRoute(route) {
5052
- const slug = route.replace(/^\//, "").split("/")[0] || "";
5053
- return MARKETING_ROUTES.has(slug);
5054
- }
5055
- function routeToFsPath(projectRoot, route, isAuth) {
5056
- const slug = route.replace(/^\//, "");
5057
- if (isAuth) {
5058
- return resolve5(projectRoot, "app", "(auth)", slug || "login", "page.tsx");
5059
- }
5060
- if (!slug) {
5061
- return resolve5(projectRoot, "app", "page.tsx");
5062
- }
5063
- if (isMarketingRoute(route)) {
5064
- return resolve5(projectRoot, "app", slug, "page.tsx");
5065
- }
5066
- return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
5067
- }
5068
- function routeToRelPath(route, isAuth) {
5069
- const slug = route.replace(/^\//, "");
5070
- if (isAuth) {
5071
- return `app/(auth)/${slug || "login"}/page.tsx`;
5072
- }
5073
- if (!slug) {
5074
- return "app/page.tsx";
5075
- }
5076
- if (isMarketingRoute(route)) {
5077
- return `app/${slug}/page.tsx`;
5078
- }
5079
- return `app/(app)/${slug}/page.tsx`;
5080
- }
5081
- function deduplicatePages(pages) {
5082
- const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
5083
- const seen = /* @__PURE__ */ new Map();
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 ids;
5097
+ return inSingle || inDouble || inTemplate;
5102
5098
  }
5103
- async function warnInlineDuplicates(projectRoot, pageName, pageCode, manifest) {
5104
- const sectionOrWidget = manifest.shared.filter((e) => e.type === "section" || e.type === "widget");
5105
- if (sectionOrWidget.length === 0) return;
5106
- for (const e of sectionOrWidget) {
5107
- const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
5108
- const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
5109
- if (hasImport) continue;
5110
- const sameNameAsTag = new RegExp(`<\\/?${e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s>]`).test(pageCode);
5111
- if (sameNameAsTag) {
5112
- console.log(
5113
- chalk8.yellow(
5114
- `
5115
- \u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
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
- if (overlap >= 12 && sharedTokens.size >= 10) {
5130
- console.log(
5131
- chalk8.yellow(
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
- } catch {
5138
- }
5139
- }
5140
- }
5141
- async function loadConfig(configPath) {
5142
- if (!existsSync13(configPath)) {
5143
- throw new Error(
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
- const codeSnippet = currentCode ? `
5174
-
5175
- Current code of ${entry.name}:
5176
- \`\`\`tsx
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 message;
5133
+ return issues;
5216
5134
  }
5217
-
5218
- // src/commands/chat/request-parser.ts
5219
- var AUTH_FLOW_PATTERNS = {
5220
- "/login": ["/register", "/forgot-password"],
5221
- "/signin": ["/register", "/forgot-password"],
5222
- "/signup": ["/login"],
5223
- "/register": ["/login"],
5224
- "/forgot-password": ["/login", "/reset-password"],
5225
- "/reset-password": ["/login"]
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
- function extractSections(code) {
5612
- const sections = [];
5613
- const seen = /* @__PURE__ */ new Set();
5614
- const commentRe = /\{\/\*\s*(.+?)\s*\*\/\}/g;
5615
- let m;
5616
- while ((m = commentRe.exec(code)) !== null) {
5617
- const raw = m[1].trim();
5618
- const name = raw.replace(/[─━—–]+/g, "").replace(/\s*section\s*$/i, "").replace(/^section\s*:\s*/i, "").trim();
5619
- if (!name || name.length <= 1 || name.length >= 40) continue;
5620
- if (seen.has(name.toLowerCase())) continue;
5621
- const wordCount = name.split(/\s+/).length;
5622
- if (wordCount > 5) continue;
5623
- if (/[{}()=<>;:`"']/.test(name)) continue;
5624
- if (/^[a-z]/.test(name) && wordCount > 2) continue;
5625
- if (VISUAL_WORDS.test(name)) continue;
5626
- seen.add(name.toLowerCase());
5627
- sections.push({ name, order: sections.length });
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
- if (sections.length === 0) {
5630
- const sectionTagRe = /<section[^>]*>[\s\S]*?<h[12][^>]*>\s*([^<]+)/g;
5631
- while ((m = sectionTagRe.exec(code)) !== null) {
5632
- const name = m[1].trim();
5633
- if (name && name.length > 1 && name.length < 40 && !seen.has(name.toLowerCase())) {
5634
- seen.add(name.toLowerCase());
5635
- sections.push({ name, order: sections.length });
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
- return sections;
5640
- }
5641
- function extractComponentUsage(code) {
5642
- const usage = {};
5643
- const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
5644
- const importedComponents = [];
5645
- let m;
5646
- while ((m = importRe.exec(code)) !== null) {
5647
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
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
- return false;
5679
- }
5680
- function summarizePageAnalysis(pageName, route, analysis) {
5681
- const parts = [`${pageName} (${route})`];
5682
- if (analysis.sections && analysis.sections.length > 0) {
5683
- parts.push(`sections: ${analysis.sections.map((s) => s.name).join(", ")}`);
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
- if (analysis.componentUsage) {
5686
- const entries = Object.entries(analysis.componentUsage).filter(([, c]) => c > 0);
5687
- if (entries.length > 0) {
5688
- parts.push(`uses: ${entries.map(([n, c]) => `${n}(${c})`).join(", ")}`);
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
- if (analysis.layoutPattern && analysis.layoutPattern !== "unknown") {
5692
- parts.push(`layout: ${analysis.layoutPattern}`);
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 (analysis.hasForm) parts.push("has-form");
5695
- return `- ${parts.join(". ")}`;
5696
- }
5697
-
5698
- // src/utils/concurrency.ts
5699
- async function pMap(items, fn, concurrency = 3) {
5700
- const results = new Array(items.length);
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 workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
5709
- await Promise.all(workers);
5710
- return results;
5711
- }
5712
-
5713
- // src/commands/chat/split-generator.ts
5714
- function buildExistingPagesContext(config2) {
5715
- const pages = config2.pages || [];
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
- return ctx;
5742
- }
5743
- function extractStyleContext(pageCode) {
5744
- const unique = (arr) => [...new Set(arr)];
5745
- 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"));
5746
- const sectionSpacing = unique(pageCode.match(/py-\d+(?:\s+md:py-\d+)?/g) || []);
5747
- const headingStyles = unique(pageCode.match(/text-(?:\d*xl|lg)\s+font-(?:bold|semibold|medium)/g) || []);
5748
- const colorPatterns = unique(
5749
- (pageCode.match(
5750
- /(?:text|bg|border)-(?:primary|secondary|muted|accent|card|destructive|foreground|background)\S*/g
5751
- ) || []).concat(
5752
- pageCode.match(
5753
- /(?:text|bg|border)-(?:emerald|blue|violet|rose|amber|zinc|slate|gray|green|red|orange|indigo|purple|teal|cyan)\S*/g
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
- const bgPatterns = unique(pageCode.match(/bg-(?:muted|card|background|zinc|slate|gray)\S*/g) || []);
5764
- const gapPatterns = unique(pageCode.match(/gap-\d+/g) || []);
5765
- const gridPatterns = unique(pageCode.match(/grid-cols-\d+|md:grid-cols-\d+|lg:grid-cols-\d+/g) || []);
5766
- const containerPatterns = unique(pageCode.match(/container\s+max-w-\S+|max-w-\d+xl\s+mx-auto/g) || []);
5767
- const lines = [];
5768
- if (containerPatterns.length > 0) {
5769
- lines.push(`Container (MUST match for alignment with header/footer): ${containerPatterns[0]} px-4`);
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
- if (cardClasses.length > 0) lines.push(`Cards: ${unique(cardClasses).slice(0, 4).join(" | ")}`);
5772
- if (sectionSpacing.length > 0) lines.push(`Section spacing: ${sectionSpacing.join(", ")}`);
5773
- if (headingStyles.length > 0) lines.push(`Headings: ${headingStyles.join(", ")}`);
5774
- if (colorPatterns.length > 0) lines.push(`Colors: ${colorPatterns.slice(0, 15).join(", ")}`);
5775
- if (iconPatterns.length > 0) lines.push(`Icon containers: ${iconPatterns.slice(0, 4).join(" | ")}`);
5776
- if (buttonPatterns.length > 0) lines.push(`Buttons: ${buttonPatterns.slice(0, 3).join(" | ")}`);
5777
- if (bgPatterns.length > 0) lines.push(`Section backgrounds: ${bgPatterns.slice(0, 6).join(", ")}`);
5778
- if (gapPatterns.length > 0) lines.push(`Gaps: ${gapPatterns.join(", ")}`);
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
- if (pageNames.length === 0) {
5801
- pageNames = extractPageNamesFromMessage(message);
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
- if (pageNames.length === 0) {
5804
- spinner.fail("Could not determine pages to create");
5805
- return [];
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
- pageNames = deduplicatePages(pageNames);
5808
- const hasHomePage = pageNames.some((p) => p.route === "/");
5809
- if (!hasHomePage) {
5810
- const userPages = (modCtx.config.pages || []).filter(
5811
- (p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
5812
- );
5813
- const isFreshProject = userPages.length === 0;
5814
- if (isFreshProject || impliesFullWebsite(message)) {
5815
- pageNames.unshift({ name: "Home", id: "home", route: "/" });
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 existingRoutes = new Set((modCtx.config.pages || []).map((p) => p.route).filter(Boolean));
5819
- const inferred = inferRelatedPages(pageNames).filter((p) => !existingRoutes.has(p.route));
5820
- if (inferred.length > 0) {
5821
- pageNames.push(...inferred);
5822
- pageNames = deduplicatePages(pageNames);
5573
+ const beforeEntityFix = fixed;
5574
+ fixed = fixed.replace(/&lt;=/g, "<=");
5575
+ fixed = fixed.replace(/&gt;=/g, ">=");
5576
+ fixed = fixed.replace(/&amp;&amp;/g, "&&");
5577
+ fixed = fixed.replace(/([\w)\]])\s*&lt;\s*([\w(])/g, "$1 < $2");
5578
+ fixed = fixed.replace(/([\w)\]])\s*&gt;\s*([\w(])/g, "$1 > $2");
5579
+ if (fixed !== beforeEntityFix) {
5580
+ fixes.push("Fixed syntax issues");
5823
5581
  }
5824
- const allRoutes = pageNames.map((p) => p.route).join(", ");
5825
- const allPagesList = pageNames.map((p) => `${p.name} (${p.route})`).join(", ");
5826
- const inferredNote = inferred.length > 0 ? ` (${inferred.length} auto-inferred)` : "";
5827
- spinner.succeed(`Phase 1/4 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
5828
- const homeIdx = pageNames.findIndex((p) => p.route === "/");
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&lt;$2");
5584
+ fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1&lt;$2");
5585
+ if (fixed !== beforeLtFix) {
5586
+ fixes.push("escaped < in JSX text content");
5847
5587
  }
5848
- if (!homeRequest) {
5849
- homeRequest = {
5850
- type: "add-page",
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
- spinner.succeed(`Phase 2/4 \u2014 ${homePage.name} page generated`);
5856
- spinner.start("Phase 3/4 \u2014 Extracting design patterns...");
5857
- const styleContext = homePageCode ? extractStyleContext(homePageCode) : "";
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 (remainingPages.length === 0) {
5865
- return [homeRequest];
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
- spinner.start(`Phase 4/4 \u2014 Generating ${remainingPages.length} pages in parallel...`);
5868
- 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.";
5869
- 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="#".`;
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 allRequests = [homeRequest, ...remainingRequests];
5902
- const withCode = allRequests.filter((r) => r.changes?.pageCode).length;
5903
- spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
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
- // src/utils/quality-validator.ts
5922
- 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;
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
- return inSingle || inDouble || inTemplate;
5962
- }
5963
- function checkLines(code, pattern, type, message, severity, skipCommentsAndStrings = false) {
5964
- const issues = [];
5965
- const lines = code.split("\n");
5966
- let inBlockComment = false;
5967
- for (let i = 0; i < lines.length; i++) {
5968
- const line = lines[i];
5969
- if (skipCommentsAndStrings) {
5970
- if (inBlockComment) {
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
- let m;
5983
- pattern.lastIndex = 0;
5984
- while ((m = pattern.exec(line)) !== null) {
5985
- if (!isInsideCommentOrString(line, m.index)) {
5986
- issues.push({ line: i + 1, type, message, severity });
5987
- break;
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
- } else {
5991
- pattern.lastIndex = 0;
5992
- if (pattern.test(line)) {
5993
- issues.push({ line: i + 1, type, message, severity });
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
- return issues;
5998
- }
5999
- function validatePageQuality(code, validRoutes) {
6000
- const issues = [];
6001
- const allLines = code.split("\n");
6002
- const isTerminalContext = (lineNum) => {
6003
- const start = Math.max(0, lineNum - 20);
6004
- const nearby = allLines.slice(start, lineNum).join(" ");
6005
- if (/font-mono/.test(allLines[lineNum - 1] || "")) return true;
6006
- if (/bg-zinc-950|bg-zinc-900/.test(nearby) && /font-mono/.test(nearby)) return true;
6007
- return false;
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
- issues.push(
6010
- ...checkLines(
6011
- code,
6012
- RAW_COLOR_RE,
6013
- "RAW_COLOR",
6014
- "Raw Tailwind color detected \u2014 use semantic tokens (bg-primary, text-muted-foreground, etc.)",
6015
- "error"
6016
- ).filter((issue) => !isTerminalContext(issue.line))
6017
- );
6018
- issues.push(
6019
- ...checkLines(
6020
- code,
6021
- HEX_IN_CLASS_RE,
6022
- "HEX_IN_CLASS",
6023
- "Hex color in className \u2014 use CSS variables via semantic tokens",
6024
- "error"
6025
- )
6026
- );
6027
- issues.push(
6028
- ...checkLines(code, TEXT_BASE_RE, "TEXT_BASE", "text-base detected \u2014 use text-sm as base font size", "warning")
6029
- );
6030
- issues.push(
6031
- ...checkLines(code, HEAVY_SHADOW_RE, "HEAVY_SHADOW", "Heavy shadow detected \u2014 use shadow-sm or none", "warning")
6032
- );
6033
- issues.push(
6034
- ...checkLines(
6035
- code,
6036
- SM_BREAKPOINT_RE,
6037
- "SM_BREAKPOINT",
6038
- "sm: breakpoint \u2014 consider if md:/lg: is sufficient",
6039
- "info"
6040
- )
6041
- );
6042
- issues.push(
6043
- ...checkLines(
6044
- code,
6045
- XL_BREAKPOINT_RE,
6046
- "XL_BREAKPOINT",
6047
- "xl: breakpoint \u2014 consider if md:/lg: is sufficient",
6048
- "info"
6049
- )
6050
- );
6051
- issues.push(
6052
- ...checkLines(
6053
- code,
6054
- XXL_BREAKPOINT_RE,
6055
- "XXL_BREAKPOINT",
6056
- "2xl: breakpoint \u2014 rarely needed, consider xl: instead",
6057
- "warning"
6058
- )
6059
- );
6060
- issues.push(
6061
- ...checkLines(
6062
- code,
6063
- LARGE_CARD_TITLE_RE,
6064
- "LARGE_CARD_TITLE",
6065
- "Large text on CardTitle \u2014 use text-sm font-medium",
6066
- "warning"
6067
- )
6068
- );
6069
- const codeLines = code.split("\n");
6070
- issues.push(
6071
- ...checkLines(
6072
- code,
6073
- RAW_BUTTON_RE,
6074
- "NATIVE_BUTTON",
6075
- "Native <button> \u2014 use Button from @/components/ui/button",
6076
- "error",
6077
- true
6078
- ).filter((issue) => {
6079
- const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
6080
- if (nearby.includes("aria-label")) return false;
6081
- if (/onClick=\{.*copy/i.test(nearby)) return false;
6082
- return true;
6083
- })
6084
- );
6085
- issues.push(
6086
- ...checkLines(
6087
- code,
6088
- RAW_SELECT_RE,
6089
- "NATIVE_SELECT",
6090
- "Native <select> \u2014 use Select from @/components/ui/select",
6091
- "error",
6092
- true
6093
- )
6094
- );
6095
- issues.push(
6096
- ...checkLines(
6097
- code,
6098
- NATIVE_CHECKBOX_RE,
6099
- "NATIVE_CHECKBOX",
6100
- 'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
6101
- "error",
6102
- true
6103
- )
6104
- );
6105
- issues.push(
6106
- ...checkLines(
6107
- code,
6108
- NATIVE_TABLE_RE,
6109
- "NATIVE_TABLE",
6110
- "Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
6111
- "warning",
6112
- true
6113
- )
6114
- );
6115
- const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
6116
- if (!hasInputImport) {
6117
- issues.push(
6118
- ...checkLines(
6119
- code,
6120
- RAW_INPUT_RE,
6121
- "RAW_INPUT",
6122
- "Raw <input> element \u2014 import and use Input from @/components/ui/input",
6123
- "warning",
6124
- true
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
- for (const pattern of PLACEHOLDER_PATTERNS) {
6129
- const lines = code.split("\n");
6130
- for (let i = 0; i < lines.length; i++) {
6131
- if (pattern.test(lines[i])) {
6132
- issues.push({
6133
- line: i + 1,
6134
- type: "PLACEHOLDER",
6135
- message: "Placeholder content detected \u2014 use real contextual content",
6136
- severity: "error"
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 hasGrid = /\bgrid\b/.test(code);
6142
- const hasResponsive = /\bmd:|lg:/.test(code);
6143
- if (hasGrid && !hasResponsive) {
6144
- issues.push({
6145
- line: 0,
6146
- type: "NO_RESPONSIVE",
6147
- message: "Grid layout without responsive breakpoints (md: or lg:)",
6148
- severity: "warning"
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
- issues.push(
6152
- ...checkLines(
6153
- code,
6154
- IMG_WITHOUT_ALT_RE,
6155
- "MISSING_ALT",
6156
- '<img> without alt attribute \u2014 add descriptive alt or alt="" for decorative images',
6157
- "error"
6158
- )
6159
- );
6160
- issues.push(
6161
- ...checkLines(
6162
- code,
6163
- GENERIC_BUTTON_LABELS,
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
- line: 0,
6173
- type: "NO_H1",
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
- } else if (h1Matches.length > 1) {
5969
+ }
5970
+ if (!after.includes("export default")) {
6178
5971
  issues.push({
6179
- line: 0,
6180
- type: "MULTIPLE_H1",
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 headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
6186
- for (let i = 1; i < headingLevels.length; i++) {
6187
- if (headingLevels[i] > headingLevels[i - 1] + 1) {
6188
- issues.push({
6189
- line: 0,
6190
- type: "SKIPPED_HEADING",
6191
- message: `Heading level skipped: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]} \u2014 don't skip levels`,
6192
- severity: "warning"
6193
- });
6194
- break;
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
- const hasLabelImport = /import\s.*Label.*from\s+['"]@\/components\/ui\//.test(code);
6198
- const inputCount = (code.match(INPUT_TAG_RE) || []).length;
6199
- const labelForCount = (code.match(LABEL_FOR_RE) || []).length;
6200
- if (hasLabelImport && inputCount > 0 && labelForCount === 0) {
6201
- issues.push({
6202
- line: 0,
6203
- type: "MISSING_LABEL",
6204
- message: "Inputs found but no Label with htmlFor \u2014 every input must have a visible label",
6205
- severity: "error"
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 (!hasLabelImport && inputCount > 0 && !/<label\b/i.test(code)) {
6209
- issues.push({
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
- const hasPlaceholder = /placeholder\s*=/.test(code);
6217
- if (hasPlaceholder && inputCount > 0 && labelForCount === 0 && !/<label\b/i.test(code) && !/<Label\b/.test(code)) {
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
- const hasInteractive = /<Button\b|<button\b|<a\b/.test(code);
6226
- const hasFocusVisible = /focus-visible:/.test(code);
6227
- const usesShadcnButton = /import\s.*Button.*from\s+['"]@\/components\/ui\//.test(code);
6228
- if (hasInteractive && !hasFocusVisible && !usesShadcnButton) {
6229
- issues.push({
6230
- line: 0,
6231
- type: "MISSING_FOCUS_VISIBLE",
6232
- message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
6233
- severity: "info"
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
- const hasTableOrList = /<Table\b|<table\b|\.map\s*\(|<ul\b|<ol\b/.test(code);
6237
- 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(
6238
- code
6239
- );
6240
- if (hasTableOrList && !hasEmptyCheck) {
6241
- issues.push({
6242
- line: 0,
6243
- type: "NO_EMPTY_STATE",
6244
- message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
6245
- severity: "warning"
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
- const hasDataFetching = /fetch\s*\(|useQuery|useSWR|useEffect\s*\([^)]*fetch|getData|loadData/i.test(code);
6249
- const hasLoadingPattern = /skeleton|Skeleton|spinner|Spinner|isLoading|loading|Loading/.test(code);
6250
- if (hasDataFetching && !hasLoadingPattern) {
6251
- issues.push({
6252
- line: 0,
6253
- type: "NO_LOADING_STATE",
6254
- message: "Page with data fetching but no loading/skeleton pattern \u2014 add skeleton or spinner",
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 hasGenericError = /Something went wrong|"Error"|'Error'|>Error<\//.test(code) || /error\.message\s*\|\|\s*["']Error["']/.test(code);
6259
- if (hasGenericError) {
6260
- issues.push({
6261
- line: 0,
6262
- type: "EMPTY_ERROR_MESSAGE",
6263
- message: "Generic error message detected \u2014 use what happened + why + what to do next",
6264
- severity: "warning"
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
- const hasDestructive = /variant\s*=\s*["']destructive["']|Delete|Remove/.test(code);
6268
- const hasConfirm = /AlertDialog|Dialog.*confirm|confirm\s*\(|onConfirm|are you sure/i.test(code);
6269
- if (hasDestructive && !hasConfirm) {
6270
- issues.push({
6271
- line: 0,
6272
- type: "DESTRUCTIVE_NO_CONFIRM",
6273
- message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
6274
- severity: "warning"
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
- const hasFormSubmit = /<form\b|onSubmit|type\s*=\s*["']submit["']/.test(code);
6278
- const hasFeedback = /toast|success|error|Saved|Saving|saving|setError|setSuccess/i.test(code);
6279
- if (hasFormSubmit && !hasFeedback) {
6280
- issues.push({
6281
- line: 0,
6282
- type: "FORM_NO_FEEDBACK",
6283
- message: 'Form with submit but no success/error feedback pattern \u2014 add "Saving..." then "Saved" or error',
6284
- severity: "info"
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
- const hasNav = /<nav\b|NavLink|navigation|sidebar.*link|Sidebar.*link/i.test(code);
6288
- const hasActiveState = /pathname|active|current|aria-current|data-active/.test(code);
6289
- if (hasNav && !hasActiveState) {
6290
- issues.push({
6291
- line: 0,
6292
- type: "NAV_NO_ACTIVE_STATE",
6293
- message: "Navigation without active/current page indicator \u2014 add active state for current route",
6294
- severity: "info"
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
- if (validRoutes && validRoutes.length > 0) {
6298
- const routeSet = new Set(validRoutes);
6299
- routeSet.add("#");
6300
- const lines = code.split("\n");
6301
- const linkHrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
6302
- for (let i = 0; i < lines.length; i++) {
6303
- let match;
6304
- while ((match = linkHrefRe.exec(lines[i])) !== null) {
6305
- const target = match[1];
6306
- if (target === "/" || target.startsWith("/design-system") || target.startsWith("/api") || target.startsWith("/#"))
6307
- continue;
6308
- if (!routeSet.has(target)) {
6309
- issues.push({
6310
- line: i + 1,
6311
- type: "BROKEN_INTERNAL_LINK",
6312
- message: `Link to "${target}" \u2014 route does not exist in project`,
6313
- severity: "warning"
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 issues;
6243
+ return inferred;
6320
6244
  }
6321
- async function autoFixCode(code) {
6322
- const fixes = [];
6323
- let fixed = code;
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(/&lt;=/g, "<=");
6331
- fixed = fixed.replace(/&gt;=/g, ">=");
6332
- fixed = fixed.replace(/&amp;&amp;/g, "&&");
6333
- fixed = fixed.replace(/([\w)\]])\s*&lt;\s*([\w(])/g, "$1 < $2");
6334
- fixed = fixed.replace(/([\w)\]])\s*&gt;\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&lt;$2");
6340
- fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1&lt;$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
- const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
6361
- if ((hasHooks || hasEvents) && !hasUseClient) {
6362
- fixed = `'use client'
6363
-
6364
- ${fixed}`;
6365
- fixes.push('added "use client" (client features detected)');
6366
- }
6367
- if (/^['"]use client['"]/.test(fixed.trim()) && /\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/.test(fixed)) {
6368
- const metaMatch = fixed.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
6369
- if (metaMatch) {
6370
- const start = fixed.indexOf(metaMatch[0]);
6371
- const open = fixed.indexOf("{", start);
6372
- let depth = 1, i = open + 1;
6373
- while (i < fixed.length && depth > 0) {
6374
- if (fixed[i] === "{") depth++;
6375
- else if (fixed[i] === "}") depth--;
6376
- i++;
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
- const lines = fixed.split("\n");
6386
- let hasReplacedButton = false;
6387
- for (let i = 0; i < lines.length; i++) {
6388
- if (!/<button\b/.test(lines[i])) continue;
6389
- if (lines[i].includes("aria-label")) continue;
6390
- if (/onClick=\{.*copy/i.test(lines[i])) continue;
6391
- const block = lines.slice(i, i + 5).join(" ");
6392
- if (block.includes("aria-label") || /onClick=\{.*copy/i.test(block)) continue;
6393
- lines[i] = lines[i].replace(/<button\b/g, "<Button");
6394
- hasReplacedButton = true;
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
- if (hasReplacedButton) {
6397
- fixed = lines.join("\n");
6398
- fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
6399
- return "</Button>";
6400
- });
6401
- const openCount = (fixed.match(/<Button\b/g) || []).length;
6402
- const closeCount = (fixed.match(/<\/Button>/g) || []).length;
6403
- if (closeCount > openCount) {
6404
- let excess = closeCount - openCount;
6405
- fixed = fixed.replace(/<\/Button>/g, (m) => {
6406
- if (excess > 0) {
6407
- excess--;
6408
- return "</button>";
6409
- }
6410
- return m;
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
- const hasButtonImport = /import\s.*\bButton\b.*from\s+['"]@\/components\/ui\/button['"]/.test(fixed);
6414
- if (!hasButtonImport) {
6415
- const lastImportIdx = fixed.lastIndexOf("\nimport ");
6416
- if (lastImportIdx !== -1) {
6417
- const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
6418
- fixed = fixed.slice(0, lineEnd + 1) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(lineEnd + 1);
6419
- } else {
6420
- const insertAfter = hasUseClient ? fixed.indexOf("\n") + 1 : 0;
6421
- fixed = fixed.slice(0, insertAfter) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(insertAfter);
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
- fixes.push("<button> \u2192 <Button> (with import)");
6425
- }
6426
- const colorMap = {
6427
- "bg-zinc-950": "bg-background",
6428
- "bg-zinc-900": "bg-background",
6429
- "bg-slate-950": "bg-background",
6430
- "bg-slate-900": "bg-background",
6431
- "bg-gray-950": "bg-background",
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 (n >= 800) {
6492
- hadColorFix = true;
6493
- return "bg-muted";
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
- if (prefix === "text") {
6497
- if (n >= 400 && n <= 600) {
6498
- hadColorFix = true;
6499
- return "text-primary";
6500
- }
6501
- if (n >= 100 && n <= 300) {
6502
- hadColorFix = true;
6503
- return "text-foreground";
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
- if (prefix === "border") {
6507
- hadColorFix = true;
6508
- return "border-primary";
6509
- }
6510
- return m;
6511
- });
6512
- const neutralColorRe = /\b(bg|text|border)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
6513
- result = result.replace(neutralColorRe, (m, prefix, _color, shade) => {
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 (n >= 100 && n <= 300) {
6525
- hadColorFix = true;
6526
- return "bg-muted";
6407
+ if (page.id !== request.target) {
6408
+ return { ...request, target: page.id };
6527
6409
  }
6528
6410
  }
6529
- if (prefix === "text") {
6530
- if (n >= 100 && n <= 300) {
6531
- hadColorFix = true;
6532
- return "text-foreground";
6533
- }
6534
- if (n >= 400 && n <= 600) {
6535
- hadColorFix = true;
6536
- return "text-muted-foreground";
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 (prefix === "border") {
6540
- hadColorFix = true;
6541
- return "border-border";
6421
+ if (sourcePage.id !== request.target) {
6422
+ return { ...request, target: sourcePage.id };
6542
6423
  }
6543
- return m;
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
- if (lucideExports) {
6560
- const nonLucideImports = /* @__PURE__ */ new Set();
6561
- for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from\s*["'](?!lucide-react)([^"']+)["']/g)) {
6562
- m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => nonLucideImports.add(n));
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
- const iconNames = lucideImportMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
6565
- const duplicates = iconNames.filter((name) => nonLucideImports.has(name));
6566
- let newImport = lucideImportMatch[1];
6567
- for (const dup of duplicates) {
6568
- newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
6569
- fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
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
- const invalid = iconNames.filter((name) => !lucideExports.has(name) && !nonLucideImports.has(name));
6572
- if (invalid.length > 0) {
6573
- const fallback = "Circle";
6574
- for (const bad of invalid) {
6575
- const re = new RegExp(`\\b${bad}\\b`, "g");
6576
- newImport = newImport.replace(re, fallback);
6577
- fixed = fixed.replace(re, fallback);
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
- fixes.push(`invalid lucide icons \u2192 ${fallback}: ${invalid.join(", ")}`);
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
- if (duplicates.length > 0 || invalid.length > 0) {
6582
- const importedNames = [
6583
- ...new Set(
6584
- newImport.split(",").map((s) => s.trim()).filter(Boolean)
6585
- )
6586
- ];
6587
- const originalImportLine = lucideImportMatch[0];
6588
- fixed = fixed.replace(originalImportLine, `import { ${importedNames.join(", ")} } from "lucide-react"`);
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
- const lucideImportMatch2 = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
6593
- if (lucideImportMatch2) {
6594
- let lucideExports2 = null;
6595
- try {
6596
- const { createRequire } = await import("module");
6597
- const req = createRequire(process.cwd() + "/package.json");
6598
- const lr = req("lucide-react");
6599
- lucideExports2 = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
6600
- } catch {
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
- if (lucideExports2) {
6603
- const allImportedNames = /* @__PURE__ */ new Set();
6604
- for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
6605
- m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => allImportedNames.add(n));
6606
- }
6607
- for (const m of fixed.matchAll(/import\s+([A-Z]\w+)\s+from/g)) {
6608
- allImportedNames.add(m[1]);
6609
- }
6610
- const lucideImported = new Set(
6611
- lucideImportMatch2[1].split(",").map((s) => s.trim()).filter(Boolean)
6612
- );
6613
- const jsxIconRefs = [...new Set([...fixed.matchAll(/<([A-Z][a-zA-Z]*Icon)\s/g)].map((m) => m[1]))];
6614
- const missing = [];
6615
- for (const ref of jsxIconRefs) {
6616
- if (allImportedNames.has(ref)) continue;
6617
- if (fixed.includes(`function ${ref}`) || fixed.includes(`const ${ref}`)) continue;
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
- fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
6645
- const cleaned = inner.replace(/\s{2,}/g, " ").trim();
6646
- return `className="${cleaned}"`;
6647
- });
6648
- let imgCounter = 1;
6649
- const beforeImgFix = fixed;
6650
- fixed = fixed.replace(/["']\/api\/placeholder\/(\d+)\/(\d+)["']/g, (_m, w, h) => {
6651
- return `"https://picsum.photos/${w}/${h}?random=${imgCounter++}"`;
6652
- });
6653
- fixed = fixed.replace(/["']\/placeholder-avatar[^"']*["']/g, () => {
6654
- return `"https://i.pravatar.cc/150?u=user${imgCounter++}"`;
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
- if (fixed !== beforeImgFix) {
6667
- fixes.push("placeholder images \u2192 working URLs (picsum/pravatar)");
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 { code: fixed, fixes };
6746
+ return "header";
6670
6747
  }
6671
- function formatIssues(issues) {
6672
- if (issues.length === 0) return "";
6673
- const errors = issues.filter((i) => i.severity === "error");
6674
- const warnings = issues.filter((i) => i.severity === "warning");
6675
- const infos = issues.filter((i) => i.severity === "info");
6676
- const lines = [];
6677
- if (errors.length > 0) {
6678
- lines.push(` \u274C ${errors.length} error(s):`);
6679
- for (const e of errors) {
6680
- lines.push(` L${e.line}: [${e.type}] ${e.message}`);
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 (warnings.length > 0) {
6684
- lines.push(` \u26A0\uFE0F ${warnings.length} warning(s):`);
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 (infos.length > 0) {
6690
- lines.push(` \u2139\uFE0F ${infos.length} info:`);
6691
- for (const i of infos) {
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
- return lines.join("\n");
6696
- }
6697
- function checkDesignConsistency(code) {
6698
- const warnings = [];
6699
- const hexPattern = /\[#[0-9a-fA-F]{3,8}\]/g;
6700
- for (const match of code.matchAll(hexPattern)) {
6701
- warnings.push({
6702
- type: "hardcoded-color",
6703
- message: `Hardcoded color ${match[0]} \u2014 use a design token (e.g., bg-primary) instead`
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 spacingPattern = /[pm][trblxy]?-\[\d+px\]/g;
6707
- for (const match of code.matchAll(spacingPattern)) {
6708
- warnings.push({
6709
- type: "arbitrary-spacing",
6710
- message: `Arbitrary spacing ${match[0]} \u2014 use Tailwind spacing scale instead`
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
- return warnings;
6714
- }
6715
- function verifyIncrementalEdit(before, after) {
6716
- const issues = [];
6717
- const hookPattern = /\buse[A-Z]\w+\s*\(/;
6718
- if (hookPattern.test(after) && !after.includes("'use client'") && !after.includes('"use client"')) {
6719
- issues.push({
6720
- type: "missing-use-client",
6721
- message: 'Code uses React hooks but missing "use client" directive'
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 (!after.includes("export default")) {
6725
- issues.push({
6726
- type: "missing-default-export",
6727
- message: "Missing default export \u2014 page component must have a default export"
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
- const importRegex = /import\s+\{([^}]+)\}\s+from/g;
6731
- const beforeImports = /* @__PURE__ */ new Set();
6732
- const afterImports = /* @__PURE__ */ new Set();
6733
- for (const match of before.matchAll(importRegex)) {
6734
- match[1].split(",").forEach((s) => beforeImports.add(s.trim()));
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
- for (const match of after.matchAll(importRegex)) {
6737
- match[1].split(",").forEach((s) => afterImports.add(s.trim()));
6832
+ if (remainingPages.length === 0) {
6833
+ return [homeRequest];
6738
6834
  }
6739
- for (const symbol of beforeImports) {
6740
- if (!afterImports.has(symbol) && symbol.length > 0) {
6741
- const codeWithoutImports = after.replace(/^import\s+.*$/gm, "");
6742
- const symbolRegex = new RegExp(`\\b${symbol}\\b`);
6743
- if (symbolRegex.test(codeWithoutImports)) {
6744
- issues.push({
6745
- type: "missing-import",
6746
- symbol,
6747
- message: `Import for "${symbol}" was removed but symbol is still used in code`
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
- return issues;
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 { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
7730
- if (spaWasFixed) {
7731
- codeToWrite = spaFixed;
7732
- autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
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
- const validRoutes = dsm.getConfig().pages.map((p) => p.route);
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 >= 5 && aiProvider) {
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, validRoutes);
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
- console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${recheckErrors.length} errors`));
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 { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
7921
- if (spaWasFixed) {
7922
- codeToWrite = spaFixed;
7923
- autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
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
- neededComponentIds.add(section.componentId);
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
- neededComponentIds.add(field.component);
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]) neededComponentIds.add(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]) neededComponentIds.add(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("\n[DEBUG] Pre-flight analysis:"));
8526
- console.log(chalk13.gray(` Needed components: ${Array.from(neededComponentIds).join(", ")}`));
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
- const INVALID_COMPONENT_IDS = /* @__PURE__ */ new Set(["ui", "shared", "lib", "utils", "hooks", "app", "components"]);
8537
- for (const id of INVALID_COMPONENT_IDS) neededComponentIds.delete(id);
8538
- const missingComponents = [];
8539
- for (const componentId of neededComponentIds) {
8540
- const exists = cm.read(componentId);
8541
- if (DEBUG4) console.log(chalk13.gray(` Checking ${componentId}: ${exists ? "EXISTS" : "MISSING"}`));
8542
- if (!exists) {
8543
- missingComponents.push(componentId);
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
- if (missingComponents.length > 0) {
8547
- spinner.stop();
8548
- console.log(chalk13.cyan("\n\u{1F50D} Pre-flight check: Installing missing components...\n"));
8549
- for (const componentId of missingComponents) {
8550
- if (DEBUG4) {
8551
- console.log(chalk13.gray(` [DEBUG] Trying to install: ${componentId}`));
8552
- console.log(chalk13.gray(` [DEBUG] isShadcnComponent(${componentId}): ${isShadcnComponent(componentId)}`));
8553
- }
8554
- if (isShadcnComponent(componentId)) {
8555
- try {
8556
- const shadcnDef = await installShadcnComponent(componentId, projectRoot);
8557
- if (DEBUG4)
8558
- console.log(chalk13.gray(` [DEBUG] shadcnDef for ${componentId}: ${shadcnDef ? "OK" : "NULL"}`));
8559
- if (shadcnDef) {
8560
- if (DEBUG4) console.log(chalk13.gray(` [DEBUG] Registering ${shadcnDef.id} (${shadcnDef.name})`));
8561
- const result = await cm.register(shadcnDef);
8562
- if (DEBUG4) {
8563
- console.log(
8564
- chalk13.gray(
8565
- ` [DEBUG] Register result: ${result.success ? "SUCCESS" : "FAILED"}${!result.success && result.message ? ` - ${result.message}` : ""}`
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
- } catch (error) {
8579
- console.log(chalk13.red(` \u274C Failed to install ${componentId}:`));
8580
- console.log(chalk13.red(` ${error instanceof Error ? error.message : error}`));
8581
- if (error instanceof Error && error.stack) {
8582
- console.log(chalk13.gray(` ${error.stack.split("\n")[1]}`));
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
- } else {
8586
- console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
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);