@getcoherent/cli 0.5.2 → 0.5.4

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 (2) hide show
  1. package/dist/index.js +2048 -1759
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -362,6 +362,7 @@ function createMinimalConfig() {
362
362
  }
363
363
  },
364
364
  settings: {
365
+ initialized: false,
365
366
  appType: "multi-page",
366
367
  framework: "next",
367
368
  typescript: true,
@@ -2427,8 +2428,8 @@ async function createAppRouteGroupLayout(projectPath) {
2427
2428
  // src/commands/chat.ts
2428
2429
  import chalk13 from "chalk";
2429
2430
  import ora2 from "ora";
2430
- import { resolve as resolve9 } from "path";
2431
- import { existsSync as existsSync16, readFileSync as readFileSync10, mkdirSync as mkdirSync6 } from "fs";
2431
+ import { resolve as resolve9, relative as relative2 } from "path";
2432
+ import { existsSync as existsSync16, readFileSync as readFileSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync2 } from "fs";
2432
2433
  import {
2433
2434
  DesignSystemManager as DesignSystemManager7,
2434
2435
  ComponentManager as ComponentManager4,
@@ -4516,6 +4517,7 @@ pageCode rules (shadcn/ui blocks quality):
4516
4517
  - 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.
4517
4518
  - Hover/focus on every interactive element (hover:bg-muted, focus-visible:ring-2 focus-visible:ring-ring).
4518
4519
  - LANGUAGE: Match the language of the user's request. English request \u2192 English page. Russian request \u2192 Russian page. Never switch languages.
4520
+ - NEVER use native HTML <select> or <option>. Always use Select, SelectTrigger, SelectValue, SelectContent, SelectItem from @/components/ui/select.
4519
4521
 
4520
4522
  NEXT.JS APP ROUTER RULE (CRITICAL \u2014 invalid code fails to compile):
4521
4523
  - "use client" and export const metadata are FORBIDDEN in the same file.
@@ -4605,6 +4607,26 @@ FEW-SHOT EXAMPLE \u2014 correct stat card in pageCode (follow this pattern exact
4605
4607
  \`\`\`
4606
4608
  Key: CardTitle is text-sm font-medium (NOT text-lg). Metric is text-2xl font-bold. Subtext is text-xs text-muted-foreground. Icon is size-4 text-muted-foreground.
4607
4609
 
4610
+ SURGICAL MODIFICATION RULES (CRITICAL for incremental edits):
4611
+ - When modifying an existing page, return the COMPLETE page code
4612
+ - Change ONLY the specific section, component, or element the user requested
4613
+ - Do NOT modify imports unless the change requires new imports
4614
+ - Do NOT change state variables, event handlers, or data in unrelated sections
4615
+ - Do NOT restyle sections the user did not mention
4616
+ - Preserve all existing className values on unchanged elements
4617
+ - If the user asks to change a "section" or "block", identify it by heading, content, or position
4618
+
4619
+ Component Promotion Rules:
4620
+ - When the user asks to "make X a shared component" or "reuse X across pages":
4621
+ - Use request type "promote-and-link"
4622
+ - Extract the JSX block into a separate component file
4623
+ - Replace inline code with the component import on all specified pages
4624
+
4625
+ Global Component Change Rules:
4626
+ - When the user asks to change "all cards" or "every button" or similar:
4627
+ - If the pattern is already a shared component, modify the shared component file
4628
+ - If the pattern is inline across pages, first promote it to a shared component, then modify it
4629
+
4608
4630
  OPTIONAL UX RECOMMENDATIONS:
4609
4631
  If you see opportunities to improve UX (accessibility, layout, consistency, responsiveness, visual hierarchy), add a short markdown block in "uxRecommendations". Otherwise omit it.
4610
4632
 
@@ -4712,8 +4734,8 @@ async function ensureAuthRouteGroup(projectRoot) {
4712
4734
  const guardPath = join6(projectRoot, "app", "ShowWhenNotAuthRoute.tsx");
4713
4735
  const rootLayoutPath = join6(projectRoot, "app", "layout.tsx");
4714
4736
  if (!existsSync9(authLayoutPath)) {
4715
- const { mkdir: mkdir7 } = await import("fs/promises");
4716
- await mkdir7(join6(projectRoot, "app", "(auth)"), { recursive: true });
4737
+ const { mkdir: mkdir8 } = await import("fs/promises");
4738
+ await mkdir8(join6(projectRoot, "app", "(auth)"), { recursive: true });
4717
4739
  await writeFile2(authLayoutPath, AUTH_LAYOUT, "utf-8");
4718
4740
  }
4719
4741
  if (!existsSync9(guardPath)) {
@@ -5021,1663 +5043,1789 @@ function fixGlobalsCss(projectRoot, config2) {
5021
5043
  writeFileSync7(layoutPath, layoutContent, "utf-8");
5022
5044
  }
5023
5045
 
5024
- // src/commands/chat/utils.ts
5025
- import { resolve as resolve5 } from "path";
5026
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
5027
- import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
5028
- import chalk8 from "chalk";
5029
- var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
5030
- function isMarketingRoute(route) {
5031
- const slug = route.replace(/^\//, "").split("/")[0] || "";
5032
- return MARKETING_ROUTES.has(slug);
5033
- }
5034
- function routeToFsPath(projectRoot, route, isAuth) {
5035
- const slug = route.replace(/^\//, "");
5036
- if (isAuth) {
5037
- return resolve5(projectRoot, "app", "(auth)", slug || "login", "page.tsx");
5038
- }
5039
- if (!slug) {
5040
- return resolve5(projectRoot, "app", "page.tsx");
5041
- }
5042
- if (isMarketingRoute(route)) {
5043
- return resolve5(projectRoot, "app", slug, "page.tsx");
5044
- }
5045
- return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
5046
- }
5047
- function routeToRelPath(route, isAuth) {
5048
- const slug = route.replace(/^\//, "");
5049
- if (isAuth) {
5050
- return `app/(auth)/${slug || "login"}/page.tsx`;
5051
- }
5052
- if (!slug) {
5053
- return "app/page.tsx";
5054
- }
5055
- if (isMarketingRoute(route)) {
5056
- return `app/${slug}/page.tsx`;
5057
- }
5058
- return `app/(app)/${slug}/page.tsx`;
5059
- }
5060
- function deduplicatePages(pages) {
5061
- const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
5062
- const seen = /* @__PURE__ */ new Map();
5063
- return pages.filter((page, idx) => {
5064
- const norm = normalize(page.route);
5065
- if (seen.has(norm)) return false;
5066
- seen.set(norm, idx);
5067
- return true;
5068
- });
5069
- }
5070
- function extractComponentIdsFromCode(code) {
5071
- const ids = /* @__PURE__ */ new Set();
5072
- const allMatches = code.matchAll(/@\/components\/((?:ui\/)?[a-z0-9-]+)/g);
5073
- for (const m of allMatches) {
5074
- if (!m[1]) continue;
5075
- let id = m[1];
5076
- if (id.startsWith("ui/")) id = id.slice(3);
5077
- if (id === "shared" || id.startsWith("shared/")) continue;
5078
- if (id) ids.add(id);
5046
+ // src/utils/quality-validator.ts
5047
+ 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;
5048
+ var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
5049
+ var TEXT_BASE_RE = /\btext-base\b/g;
5050
+ var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
5051
+ var SM_BREAKPOINT_RE = /\bsm:/g;
5052
+ var XL_BREAKPOINT_RE = /\bxl:/g;
5053
+ var XXL_BREAKPOINT_RE = /\b2xl:/g;
5054
+ var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
5055
+ var RAW_BUTTON_RE = /<button\b/g;
5056
+ var RAW_INPUT_RE = /<input\b/g;
5057
+ var RAW_SELECT_RE = /<select\b/g;
5058
+ var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
5059
+ var NATIVE_TABLE_RE = /<table\b/g;
5060
+ var PLACEHOLDER_PATTERNS = [
5061
+ />\s*Lorem ipsum\b/i,
5062
+ />\s*Card content\s*</i,
5063
+ />\s*Your (?:text|content) here\s*</i,
5064
+ />\s*Description\s*</,
5065
+ />\s*Title\s*</,
5066
+ /placeholder\s*text/i
5067
+ ];
5068
+ var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
5069
+ var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
5070
+ var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
5071
+ var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
5072
+ function isInsideCommentOrString(line, matchIndex) {
5073
+ const commentIdx = line.indexOf("//");
5074
+ if (commentIdx !== -1 && commentIdx < matchIndex) return true;
5075
+ let inSingle = false;
5076
+ let inDouble = false;
5077
+ let inTemplate = false;
5078
+ for (let i = 0; i < matchIndex; i++) {
5079
+ const ch = line[i];
5080
+ const prev = i > 0 ? line[i - 1] : "";
5081
+ if (prev === "\\") continue;
5082
+ if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
5083
+ if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
5084
+ if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
5079
5085
  }
5080
- return ids;
5086
+ return inSingle || inDouble || inTemplate;
5081
5087
  }
5082
- async function warnInlineDuplicates(projectRoot, pageName, pageCode, manifest) {
5083
- const sectionOrWidget = manifest.shared.filter((e) => e.type === "section" || e.type === "widget");
5084
- if (sectionOrWidget.length === 0) return;
5085
- for (const e of sectionOrWidget) {
5086
- const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
5087
- const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
5088
- if (hasImport) continue;
5089
- const sameNameAsTag = new RegExp(`<\\/?${e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s>]`).test(pageCode);
5090
- if (sameNameAsTag) {
5091
- console.log(
5092
- chalk8.yellow(
5093
- `
5094
- \u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
5095
- )
5096
- );
5097
- continue;
5098
- }
5099
- try {
5100
- const fullPath = resolve5(projectRoot, e.file);
5101
- const sharedSnippet = (await readFile(fullPath)).slice(0, 600);
5102
- const sharedTokens = new Set(sharedSnippet.match(/\b[a-zA-Z0-9-]{4,}\b/g) ?? []);
5103
- const pageTokens = pageCode.match(/\b[a-zA-Z0-9-]+\b/g) ?? [];
5104
- let overlap = 0;
5105
- for (const t of sharedTokens) {
5106
- if (pageTokens.includes(t)) overlap++;
5088
+ function checkLines(code, pattern, type, message, severity, skipCommentsAndStrings = false) {
5089
+ const issues = [];
5090
+ const lines = code.split("\n");
5091
+ let inBlockComment = false;
5092
+ for (let i = 0; i < lines.length; i++) {
5093
+ const line = lines[i];
5094
+ if (skipCommentsAndStrings) {
5095
+ if (inBlockComment) {
5096
+ const endIdx = line.indexOf("*/");
5097
+ if (endIdx !== -1) {
5098
+ inBlockComment = false;
5099
+ }
5100
+ continue;
5107
5101
  }
5108
- if (overlap >= 12 && sharedTokens.size >= 10) {
5109
- console.log(
5110
- chalk8.yellow(
5111
- `
5112
- \u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
5113
- )
5114
- );
5102
+ const blockStart = line.indexOf("/*");
5103
+ if (blockStart !== -1 && !line.includes("*/")) {
5104
+ inBlockComment = true;
5105
+ continue;
5115
5106
  }
5116
- } catch {
5117
- }
5118
- }
5119
- }
5120
- async function loadConfig(configPath) {
5121
- if (!existsSync13(configPath)) {
5122
- throw new Error(
5123
- `Design system config not found at ${configPath}
5124
- Run "coherent init" first to create a project.`
5125
- );
5126
- }
5127
- const manager = new DesignSystemManager3(configPath);
5128
- await manager.load();
5129
- return manager.getConfig();
5130
- }
5131
- function requireProject() {
5132
- const project = findConfig();
5133
- if (!project) {
5134
- exitNotCoherent();
5135
- }
5136
- warnIfVolatile(project.root);
5137
- return project;
5138
- }
5139
- async function resolveTargetFlags(message, options, config2, projectRoot) {
5140
- if (options.component) {
5141
- const manifest = await loadManifest4(projectRoot);
5142
- const target = options.component;
5143
- const entry = manifest.shared.find(
5144
- (s) => s.name.toLowerCase() === target.toLowerCase() || s.id.toLowerCase() === target.toLowerCase()
5145
- );
5146
- if (entry) {
5147
- const filePath = resolve5(projectRoot, entry.file);
5148
- let currentCode = "";
5149
- if (existsSync13(filePath)) {
5150
- currentCode = readFileSync8(filePath, "utf-8");
5107
+ let m;
5108
+ pattern.lastIndex = 0;
5109
+ while ((m = pattern.exec(line)) !== null) {
5110
+ if (!isInsideCommentOrString(line, m.index)) {
5111
+ issues.push({ line: i + 1, type, message, severity });
5112
+ break;
5113
+ }
5151
5114
  }
5152
- const codeSnippet = currentCode ? `
5153
-
5154
- Current code of ${entry.name}:
5155
- \`\`\`tsx
5156
- ${currentCode}
5157
- \`\`\`` : "";
5158
- 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}`;
5159
- }
5160
- console.log(chalk8.yellow(`
5161
- \u26A0\uFE0F Component "${target}" not found in shared components.`));
5162
- console.log(chalk8.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
5163
- console.log(chalk8.dim(" Proceeding with message as-is...\n"));
5164
- }
5165
- if (options.page) {
5166
- const target = options.page;
5167
- const page = config2.pages.find(
5168
- (p) => p.name.toLowerCase() === target.toLowerCase() || p.id.toLowerCase() === target.toLowerCase() || p.route === target || p.route === "/" + target
5169
- );
5170
- if (page) {
5171
- const relPath = page.route === "/" ? "app/page.tsx" : `app${page.route}/page.tsx`;
5172
- const filePath = resolve5(projectRoot, relPath);
5173
- let currentCode = "";
5174
- if (existsSync13(filePath)) {
5175
- currentCode = readFileSync8(filePath, "utf-8");
5115
+ } else {
5116
+ pattern.lastIndex = 0;
5117
+ if (pattern.test(line)) {
5118
+ issues.push({ line: i + 1, type, message, severity });
5176
5119
  }
5177
- const codeSnippet = currentCode ? `
5178
-
5179
- Current code of ${page.name} page:
5180
- \`\`\`tsx
5181
- ${currentCode}
5182
- \`\`\`` : "";
5183
- 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}`;
5184
5120
  }
5185
- console.log(chalk8.yellow(`
5186
- \u26A0\uFE0F Page "${target}" not found.`));
5187
- console.log(chalk8.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
5188
- console.log(chalk8.dim(" Proceeding with message as-is...\n"));
5189
- }
5190
- if (options.token) {
5191
- const target = options.token;
5192
- return `Change design token "${target}": ${message}. Update the token value in design-system.config.ts and ensure globals.css reflects the change.`;
5193
5121
  }
5194
- return message;
5122
+ return issues;
5195
5123
  }
5196
-
5197
- // src/commands/chat/request-parser.ts
5198
- var AUTH_FLOW_PATTERNS = {
5199
- "/login": ["/register", "/forgot-password"],
5200
- "/signin": ["/register", "/forgot-password"],
5201
- "/signup": ["/login"],
5202
- "/register": ["/login"],
5203
- "/forgot-password": ["/login", "/reset-password"],
5204
- "/reset-password": ["/login"]
5205
- };
5206
- var PAGE_RELATIONSHIP_RULES = [
5207
- {
5208
- trigger: /\/(products|catalog|marketplace|listings|shop|store)\b/i,
5209
- related: [{ id: "product-detail", name: "Product Detail", route: "/products/[id]" }]
5210
- },
5211
- {
5212
- trigger: /\/(blog|news|articles|posts)\b/i,
5213
- related: [{ id: "article-detail", name: "Article", route: "/blog/[slug]" }]
5214
- },
5215
- {
5216
- trigger: /\/(campaigns|ads|ad-campaigns)\b/i,
5217
- related: [{ id: "campaign-detail", name: "Campaign Detail", route: "/campaigns/[id]" }]
5218
- },
5219
- {
5220
- trigger: /\/(dashboard|admin)\b/i,
5221
- related: [{ id: "settings", name: "Settings", route: "/settings" }]
5222
- },
5223
- {
5224
- trigger: /\/pricing\b/i,
5225
- related: [{ id: "checkout", name: "Checkout", route: "/checkout" }]
5226
- }
5227
- ];
5228
- function extractInternalLinks(code) {
5229
- const links = /* @__PURE__ */ new Set();
5230
- const hrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
5231
- let m;
5232
- while ((m = hrefRe.exec(code)) !== null) {
5233
- const route = m[1];
5234
- if (route === "/" || route.startsWith("/design-system") || route.startsWith("/#") || route.startsWith("/api"))
5235
- continue;
5236
- links.add(route);
5237
- }
5238
- return [...links];
5239
- }
5240
- function inferRelatedPages(plannedPages) {
5241
- const plannedRoutes = new Set(plannedPages.map((p) => p.route));
5242
- const inferred = [];
5243
- for (const { route } of plannedPages) {
5244
- const authRelated = AUTH_FLOW_PATTERNS[route];
5245
- if (authRelated) {
5246
- for (const rel of authRelated) {
5247
- if (!plannedRoutes.has(rel)) {
5248
- const slug = rel.slice(1);
5249
- const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
5250
- inferred.push({ id: slug, name, route: rel });
5251
- plannedRoutes.add(rel);
5252
- }
5253
- }
5254
- }
5255
- for (const rule of PAGE_RELATIONSHIP_RULES) {
5256
- if (rule.trigger.test(route)) {
5257
- for (const rel of rule.related) {
5258
- if (!plannedRoutes.has(rel.route)) {
5259
- inferred.push(rel);
5260
- plannedRoutes.add(rel.route);
5261
- }
5262
- }
5263
- }
5264
- }
5265
- }
5266
- return inferred;
5267
- }
5268
- function impliesFullWebsite(message) {
5269
- 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(
5270
- message
5271
- );
5272
- }
5273
- function extractPageNamesFromMessage(message) {
5274
- const pages = [];
5275
- const known = {
5276
- home: "/",
5277
- landing: "/",
5278
- dashboard: "/dashboard",
5279
- about: "/about",
5280
- "about us": "/about",
5281
- contact: "/contact",
5282
- contacts: "/contacts",
5283
- pricing: "/pricing",
5284
- settings: "/settings",
5285
- account: "/account",
5286
- "personal account": "/account",
5287
- registration: "/registration",
5288
- signup: "/signup",
5289
- "sign up": "/signup",
5290
- login: "/login",
5291
- "sign in": "/login",
5292
- catalogue: "/catalogue",
5293
- catalog: "/catalog",
5294
- blog: "/blog",
5295
- portfolio: "/portfolio",
5296
- features: "/features",
5297
- services: "/services",
5298
- faq: "/faq",
5299
- team: "/team"
5124
+ function validatePageQuality(code, validRoutes) {
5125
+ const issues = [];
5126
+ const allLines = code.split("\n");
5127
+ const isTerminalContext = (lineNum) => {
5128
+ const start = Math.max(0, lineNum - 20);
5129
+ const nearby = allLines.slice(start, lineNum).join(" ");
5130
+ if (/font-mono/.test(allLines[lineNum - 1] || "")) return true;
5131
+ if (/bg-zinc-950|bg-zinc-900/.test(nearby) && /font-mono/.test(nearby)) return true;
5132
+ return false;
5300
5133
  };
5301
- const lower = message.toLowerCase();
5302
- for (const [key, route] of Object.entries(known)) {
5303
- if (lower.includes(key)) {
5304
- const name = key.split(" ").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
5305
- const id = route.slice(1) || "home";
5306
- if (!pages.some((p) => p.route === route)) {
5307
- pages.push({ name, id, route });
5134
+ issues.push(
5135
+ ...checkLines(
5136
+ code,
5137
+ RAW_COLOR_RE,
5138
+ "RAW_COLOR",
5139
+ "Raw Tailwind color detected \u2014 use semantic tokens (bg-primary, text-muted-foreground, etc.)",
5140
+ "error"
5141
+ ).filter((issue) => !isTerminalContext(issue.line))
5142
+ );
5143
+ issues.push(
5144
+ ...checkLines(
5145
+ code,
5146
+ HEX_IN_CLASS_RE,
5147
+ "HEX_IN_CLASS",
5148
+ "Hex color in className \u2014 use CSS variables via semantic tokens",
5149
+ "error"
5150
+ )
5151
+ );
5152
+ issues.push(
5153
+ ...checkLines(code, TEXT_BASE_RE, "TEXT_BASE", "text-base detected \u2014 use text-sm as base font size", "warning")
5154
+ );
5155
+ issues.push(
5156
+ ...checkLines(code, HEAVY_SHADOW_RE, "HEAVY_SHADOW", "Heavy shadow detected \u2014 use shadow-sm or none", "warning")
5157
+ );
5158
+ issues.push(
5159
+ ...checkLines(
5160
+ code,
5161
+ SM_BREAKPOINT_RE,
5162
+ "SM_BREAKPOINT",
5163
+ "sm: breakpoint \u2014 consider if md:/lg: is sufficient",
5164
+ "info"
5165
+ )
5166
+ );
5167
+ issues.push(
5168
+ ...checkLines(
5169
+ code,
5170
+ XL_BREAKPOINT_RE,
5171
+ "XL_BREAKPOINT",
5172
+ "xl: breakpoint \u2014 consider if md:/lg: is sufficient",
5173
+ "info"
5174
+ )
5175
+ );
5176
+ issues.push(
5177
+ ...checkLines(
5178
+ code,
5179
+ XXL_BREAKPOINT_RE,
5180
+ "XXL_BREAKPOINT",
5181
+ "2xl: breakpoint \u2014 rarely needed, consider xl: instead",
5182
+ "warning"
5183
+ )
5184
+ );
5185
+ issues.push(
5186
+ ...checkLines(
5187
+ code,
5188
+ LARGE_CARD_TITLE_RE,
5189
+ "LARGE_CARD_TITLE",
5190
+ "Large text on CardTitle \u2014 use text-sm font-medium",
5191
+ "warning"
5192
+ )
5193
+ );
5194
+ const codeLines = code.split("\n");
5195
+ issues.push(
5196
+ ...checkLines(
5197
+ code,
5198
+ RAW_BUTTON_RE,
5199
+ "NATIVE_BUTTON",
5200
+ "Native <button> \u2014 use Button from @/components/ui/button",
5201
+ "error",
5202
+ true
5203
+ ).filter((issue) => {
5204
+ const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
5205
+ if (nearby.includes("aria-label")) return false;
5206
+ if (/onClick=\{.*copy/i.test(nearby)) return false;
5207
+ return true;
5208
+ })
5209
+ );
5210
+ issues.push(
5211
+ ...checkLines(
5212
+ code,
5213
+ RAW_SELECT_RE,
5214
+ "NATIVE_SELECT",
5215
+ "Native <select> \u2014 use Select from @/components/ui/select",
5216
+ "error",
5217
+ true
5218
+ )
5219
+ );
5220
+ issues.push(
5221
+ ...checkLines(
5222
+ code,
5223
+ NATIVE_CHECKBOX_RE,
5224
+ "NATIVE_CHECKBOX",
5225
+ 'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
5226
+ "error",
5227
+ true
5228
+ )
5229
+ );
5230
+ issues.push(
5231
+ ...checkLines(
5232
+ code,
5233
+ NATIVE_TABLE_RE,
5234
+ "NATIVE_TABLE",
5235
+ "Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
5236
+ "warning",
5237
+ true
5238
+ )
5239
+ );
5240
+ const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
5241
+ if (!hasInputImport) {
5242
+ issues.push(
5243
+ ...checkLines(
5244
+ code,
5245
+ RAW_INPUT_RE,
5246
+ "RAW_INPUT",
5247
+ "Raw <input> element \u2014 import and use Input from @/components/ui/input",
5248
+ "warning",
5249
+ true
5250
+ )
5251
+ );
5252
+ }
5253
+ for (const pattern of PLACEHOLDER_PATTERNS) {
5254
+ const lines = code.split("\n");
5255
+ for (let i = 0; i < lines.length; i++) {
5256
+ if (pattern.test(lines[i])) {
5257
+ issues.push({
5258
+ line: i + 1,
5259
+ type: "PLACEHOLDER",
5260
+ message: "Placeholder content detected \u2014 use real contextual content",
5261
+ severity: "error"
5262
+ });
5308
5263
  }
5309
5264
  }
5310
5265
  }
5311
- return pages;
5312
- }
5313
- function normalizeRequest(request, config2) {
5314
- const changes = request.changes;
5315
- const VALID_TYPES = [
5316
- "update-token",
5317
- "add-component",
5318
- "modify-component",
5319
- "add-layout-block",
5320
- "modify-layout-block",
5321
- "add-page",
5322
- "update-page",
5323
- "update-navigation",
5324
- "link-shared",
5325
- "promote-and-link"
5326
- ];
5327
- if (!VALID_TYPES.includes(request.type)) {
5328
- return { error: `Unknown action "${request.type}". Valid: ${VALID_TYPES.join(", ")}` };
5266
+ const hasGrid = /\bgrid\b/.test(code);
5267
+ const hasResponsive = /\bmd:|lg:/.test(code);
5268
+ if (hasGrid && !hasResponsive) {
5269
+ issues.push({
5270
+ line: 0,
5271
+ type: "NO_RESPONSIVE",
5272
+ message: "Grid layout without responsive breakpoints (md: or lg:)",
5273
+ severity: "warning"
5274
+ });
5329
5275
  }
5330
- const findPage = (target) => config2.pages.find(
5331
- (p) => p.id === target || p.route === target || p.name?.toLowerCase() === String(target).toLowerCase()
5332
- );
5333
- switch (request.type) {
5334
- case "update-page": {
5335
- const page = findPage(request.target);
5336
- if (!page && changes?.pageCode) {
5337
- const targetStr = String(request.target);
5338
- const id = targetStr.replace(/^\//, "") || "home";
5339
- return {
5340
- ...request,
5341
- type: "add-page",
5342
- target: "new",
5343
- changes: {
5344
- id,
5345
- name: changes.name || id.charAt(0).toUpperCase() + id.slice(1) || "Home",
5346
- route: targetStr.startsWith("/") ? targetStr : `/${targetStr}`,
5347
- ...changes
5348
- }
5349
- };
5350
- }
5351
- if (!page) {
5352
- const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
5353
- return { error: `Page "${request.target}" not found. Available: ${available || "none"}` };
5354
- }
5355
- if (page.id !== request.target) {
5356
- return { ...request, target: page.id };
5357
- }
5358
- break;
5359
- }
5360
- case "add-page": {
5361
- if (!changes) break;
5362
- let route = changes.route || "";
5363
- if (route && !route.startsWith("/")) route = `/${route}`;
5364
- if (route) changes.route = route;
5365
- const existingByRoute = config2.pages.find((p) => p.route === route);
5366
- if (existingByRoute && route) {
5367
- return {
5368
- ...request,
5369
- type: "update-page",
5370
- target: existingByRoute.id
5371
- };
5372
- }
5373
- if (!changes.id && changes.name) {
5374
- changes.id = String(changes.name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
5375
- }
5376
- if (!changes.id && route) {
5377
- changes.id = route.replace(/^\//, "") || "home";
5378
- }
5379
- break;
5380
- }
5381
- case "modify-component": {
5382
- const componentId = request.target;
5383
- const existingComp = config2.components.find((c) => c.id === componentId);
5384
- if (!existingComp) {
5385
- return {
5386
- ...request,
5387
- type: "add-component",
5388
- target: "new"
5389
- };
5390
- }
5391
- if (changes) {
5392
- if (typeof changes.id === "string" && changes.id !== componentId) {
5393
- const targetExists = config2.components.some((c) => c.id === changes.id);
5394
- if (!targetExists) {
5395
- return { ...request, type: "add-component", target: "new" };
5396
- }
5397
- return {
5398
- error: `Cannot change component "${componentId}" to "${changes.id}" \u2014 "${changes.id}" already exists.`
5399
- };
5400
- }
5401
- if (typeof changes.name === "string") {
5402
- const newName = changes.name.toLowerCase();
5403
- const curName = existingComp.name.toLowerCase();
5404
- const curId = componentId.toLowerCase();
5405
- const nameOk = newName === curName || newName === curId || newName.includes(curId) || curId.includes(newName);
5406
- if (!nameOk) {
5407
- delete changes.name;
5408
- }
5409
- }
5410
- }
5411
- break;
5412
- }
5413
- case "add-component": {
5414
- if (changes) {
5415
- const shadcn = changes.shadcnComponent;
5416
- const id = changes.id;
5417
- if (shadcn && id && id !== shadcn) {
5418
- changes.id = shadcn;
5419
- }
5420
- }
5421
- break;
5422
- }
5423
- case "link-shared": {
5424
- if (changes) {
5425
- const page = findPage(request.target);
5426
- if (!page) {
5427
- const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
5428
- return { error: `Page "${request.target}" not found for link-shared. Available: ${available || "none"}` };
5429
- }
5430
- if (page.id !== request.target) {
5431
- return { ...request, target: page.id };
5432
- }
5433
- }
5434
- break;
5435
- }
5436
- case "promote-and-link": {
5437
- const sourcePage = findPage(request.target);
5438
- if (!sourcePage) {
5439
- const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
5440
- return {
5441
- error: `Source page "${request.target}" not found for promote-and-link. Available: ${available || "none"}`
5442
- };
5443
- }
5444
- if (sourcePage.id !== request.target) {
5445
- return { ...request, target: sourcePage.id };
5446
- }
5276
+ issues.push(
5277
+ ...checkLines(
5278
+ code,
5279
+ IMG_WITHOUT_ALT_RE,
5280
+ "MISSING_ALT",
5281
+ '<img> without alt attribute \u2014 add descriptive alt or alt="" for decorative images',
5282
+ "error"
5283
+ )
5284
+ );
5285
+ issues.push(
5286
+ ...checkLines(
5287
+ code,
5288
+ GENERIC_BUTTON_LABELS,
5289
+ "GENERIC_BUTTON_TEXT",
5290
+ 'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
5291
+ "warning"
5292
+ )
5293
+ );
5294
+ const h1Matches = code.match(/<h1[\s>]/g);
5295
+ if (!h1Matches || h1Matches.length === 0) {
5296
+ issues.push({
5297
+ line: 0,
5298
+ type: "NO_H1",
5299
+ message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
5300
+ severity: "warning"
5301
+ });
5302
+ } else if (h1Matches.length > 1) {
5303
+ issues.push({
5304
+ line: 0,
5305
+ type: "MULTIPLE_H1",
5306
+ message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
5307
+ severity: "warning"
5308
+ });
5309
+ }
5310
+ const headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
5311
+ for (let i = 1; i < headingLevels.length; i++) {
5312
+ if (headingLevels[i] > headingLevels[i - 1] + 1) {
5313
+ issues.push({
5314
+ line: 0,
5315
+ type: "SKIPPED_HEADING",
5316
+ message: `Heading level skipped: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]} \u2014 don't skip levels`,
5317
+ severity: "warning"
5318
+ });
5447
5319
  break;
5448
5320
  }
5449
5321
  }
5450
- return request;
5451
- }
5452
- function applyDefaults(request) {
5453
- if (request.type === "add-page" && request.changes && typeof request.changes === "object") {
5454
- const changes = request.changes;
5455
- const now = (/* @__PURE__ */ new Date()).toISOString();
5456
- const name = changes.name || "New Page";
5457
- let id = changes.id || name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
5458
- if (!/^[a-z]/.test(id)) id = `page-${id}`;
5459
- const route = changes.route || `/${id}`;
5460
- const hasPageCode = typeof changes.pageCode === "string" && changes.pageCode.trim() !== "";
5461
- const base = {
5462
- id,
5463
- name,
5464
- route: route.startsWith("/") ? route : `/${route}`,
5465
- layout: changes.layout || "centered",
5466
- title: changes.title || name,
5467
- description: changes.description || `${name} page`,
5468
- createdAt: changes.createdAt || now,
5469
- updatedAt: changes.updatedAt || now,
5470
- requiresAuth: changes.requiresAuth ?? false,
5471
- noIndex: changes.noIndex ?? false
5472
- };
5473
- const sections = Array.isArray(changes.sections) ? changes.sections.map((section, idx) => ({
5474
- id: section.id || `section-${idx}`,
5475
- name: section.name || `Section ${idx + 1}`,
5476
- componentId: section.componentId || "button",
5477
- order: typeof section.order === "number" ? section.order : idx,
5478
- props: section.props || {}
5479
- })) : [];
5480
- return {
5481
- ...request,
5482
- changes: {
5483
- ...base,
5484
- sections,
5485
- ...hasPageCode ? { pageCode: changes.pageCode, generatedWithPageCode: true } : {},
5486
- ...changes.pageType ? { pageType: changes.pageType } : {},
5487
- ...changes.structuredContent ? { structuredContent: changes.structuredContent } : {}
5488
- }
5489
- };
5322
+ const hasLabelImport = /import\s.*Label.*from\s+['"]@\/components\/ui\//.test(code);
5323
+ const inputCount = (code.match(INPUT_TAG_RE) || []).length;
5324
+ const labelForCount = (code.match(LABEL_FOR_RE) || []).length;
5325
+ if (hasLabelImport && inputCount > 0 && labelForCount === 0) {
5326
+ issues.push({
5327
+ line: 0,
5328
+ type: "MISSING_LABEL",
5329
+ message: "Inputs found but no Label with htmlFor \u2014 every input must have a visible label",
5330
+ severity: "error"
5331
+ });
5490
5332
  }
5491
- if (request.type === "add-component" && request.changes && typeof request.changes === "object") {
5492
- const changes = request.changes;
5493
- const now = (/* @__PURE__ */ new Date()).toISOString();
5494
- const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
5495
- let normalizedVariants = [];
5496
- if (Array.isArray(changes.variants)) {
5497
- normalizedVariants = changes.variants.map((v) => {
5498
- if (typeof v === "string") return { name: v, className: "" };
5499
- if (v && typeof v === "object" && "name" in v) {
5500
- return {
5501
- name: v.name,
5502
- className: v.className ?? ""
5503
- };
5504
- }
5505
- return { name: "default", className: "" };
5506
- });
5507
- }
5508
- let normalizedSizes = [];
5509
- if (Array.isArray(changes.sizes)) {
5510
- normalizedSizes = changes.sizes.map((s) => {
5511
- if (typeof s === "string") {
5512
- const name = validSizeNames.includes(s) ? s : "md";
5513
- return { name, className: "" };
5514
- }
5515
- if (s && typeof s === "object" && "name" in s) {
5516
- const raw = s.name;
5517
- const name = validSizeNames.includes(raw) ? raw : "md";
5518
- return { name, className: s.className ?? "" };
5519
- }
5520
- return { name: "md", className: "" };
5521
- });
5522
- }
5523
- return {
5524
- ...request,
5525
- changes: {
5526
- ...changes,
5527
- variants: normalizedVariants,
5528
- sizes: normalizedSizes,
5529
- createdAt: now,
5530
- updatedAt: now
5531
- }
5532
- };
5333
+ if (!hasLabelImport && inputCount > 0 && !/<label\b/i.test(code)) {
5334
+ issues.push({
5335
+ line: 0,
5336
+ type: "MISSING_LABEL",
5337
+ message: "Inputs found but no Label component \u2014 import Label and add htmlFor on each input",
5338
+ severity: "error"
5339
+ });
5533
5340
  }
5534
- if (request.type === "modify-component" && request.changes && typeof request.changes === "object") {
5535
- const changes = request.changes;
5536
- const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
5537
- let normalizedVariants;
5538
- if (Array.isArray(changes.variants)) {
5539
- normalizedVariants = changes.variants.map((v) => {
5540
- if (typeof v === "string") return { name: v, className: "" };
5541
- if (v && typeof v === "object" && "name" in v) {
5542
- return {
5543
- name: v.name,
5544
- className: v.className ?? ""
5545
- };
5546
- }
5547
- return { name: "default", className: "" };
5548
- });
5549
- }
5550
- let normalizedSizes;
5551
- if (Array.isArray(changes.sizes)) {
5552
- normalizedSizes = changes.sizes.map((s) => {
5553
- if (typeof s === "string") {
5554
- const name = validSizeNames.includes(s) ? s : "md";
5555
- return { name, className: "" };
5556
- }
5557
- if (s && typeof s === "object" && "name" in s) {
5558
- const raw = s.name;
5559
- const name = validSizeNames.includes(raw) ? raw : "md";
5560
- return { name, className: s.className ?? "" };
5561
- }
5562
- return { name: "md", className: "" };
5563
- });
5564
- }
5565
- return {
5566
- ...request,
5567
- changes: {
5568
- ...changes,
5569
- ...normalizedVariants !== void 0 && { variants: normalizedVariants },
5570
- ...normalizedSizes !== void 0 && { sizes: normalizedSizes }
5571
- }
5572
- };
5341
+ const hasPlaceholder = /placeholder\s*=/.test(code);
5342
+ if (hasPlaceholder && inputCount > 0 && labelForCount === 0 && !/<label\b/i.test(code) && !/<Label\b/.test(code)) {
5343
+ issues.push({
5344
+ line: 0,
5345
+ type: "PLACEHOLDER_ONLY_LABEL",
5346
+ message: "Inputs use placeholder only \u2014 add visible Label with htmlFor (placeholder is not a substitute)",
5347
+ severity: "error"
5348
+ });
5573
5349
  }
5574
- return request;
5575
- }
5576
-
5577
- // src/utils/page-analyzer.ts
5578
- var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
5579
- 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;
5580
- function analyzePageCode(code) {
5581
- return {
5582
- sections: extractSections(code),
5583
- componentUsage: extractComponentUsage(code),
5584
- iconCount: extractIconCount(code),
5585
- layoutPattern: inferLayoutPattern(code),
5586
- hasForm: detectFormUsage(code),
5587
- analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
5588
- };
5589
- }
5590
- function extractSections(code) {
5591
- const sections = [];
5592
- const seen = /* @__PURE__ */ new Set();
5593
- const commentRe = /\{\/\*\s*(.+?)\s*\*\/\}/g;
5594
- let m;
5595
- while ((m = commentRe.exec(code)) !== null) {
5596
- const raw = m[1].trim();
5597
- const name = raw.replace(/[─━—–]+/g, "").replace(/\s*section\s*$/i, "").replace(/^section\s*:\s*/i, "").trim();
5598
- if (!name || name.length <= 1 || name.length >= 40) continue;
5599
- if (seen.has(name.toLowerCase())) continue;
5600
- const wordCount = name.split(/\s+/).length;
5601
- if (wordCount > 5) continue;
5602
- if (/[{}()=<>;:`"']/.test(name)) continue;
5603
- if (/^[a-z]/.test(name) && wordCount > 2) continue;
5604
- if (VISUAL_WORDS.test(name)) continue;
5605
- seen.add(name.toLowerCase());
5606
- sections.push({ name, order: sections.length });
5350
+ const hasInteractive = /<Button\b|<button\b|<a\b/.test(code);
5351
+ const hasFocusVisible = /focus-visible:/.test(code);
5352
+ const usesShadcnButton = /import\s.*Button.*from\s+['"]@\/components\/ui\//.test(code);
5353
+ if (hasInteractive && !hasFocusVisible && !usesShadcnButton) {
5354
+ issues.push({
5355
+ line: 0,
5356
+ type: "MISSING_FOCUS_VISIBLE",
5357
+ message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
5358
+ severity: "info"
5359
+ });
5607
5360
  }
5608
- if (sections.length === 0) {
5609
- const sectionTagRe = /<section[^>]*>[\s\S]*?<h[12][^>]*>\s*([^<]+)/g;
5610
- while ((m = sectionTagRe.exec(code)) !== null) {
5611
- const name = m[1].trim();
5612
- if (name && name.length > 1 && name.length < 40 && !seen.has(name.toLowerCase())) {
5613
- seen.add(name.toLowerCase());
5614
- sections.push({ name, order: sections.length });
5361
+ const hasTableOrList = /<Table\b|<table\b|\.map\s*\(|<ul\b|<ol\b/.test(code);
5362
+ 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(
5363
+ code
5364
+ );
5365
+ if (hasTableOrList && !hasEmptyCheck) {
5366
+ issues.push({
5367
+ line: 0,
5368
+ type: "NO_EMPTY_STATE",
5369
+ message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
5370
+ severity: "warning"
5371
+ });
5372
+ }
5373
+ const hasDataFetching = /fetch\s*\(|useQuery|useSWR|useEffect\s*\([^)]*fetch|getData|loadData/i.test(code);
5374
+ const hasLoadingPattern = /skeleton|Skeleton|spinner|Spinner|isLoading|loading|Loading/.test(code);
5375
+ if (hasDataFetching && !hasLoadingPattern) {
5376
+ issues.push({
5377
+ line: 0,
5378
+ type: "NO_LOADING_STATE",
5379
+ message: "Page with data fetching but no loading/skeleton pattern \u2014 add skeleton or spinner",
5380
+ severity: "warning"
5381
+ });
5382
+ }
5383
+ const hasGenericError = /Something went wrong|"Error"|'Error'|>Error<\//.test(code) || /error\.message\s*\|\|\s*["']Error["']/.test(code);
5384
+ if (hasGenericError) {
5385
+ issues.push({
5386
+ line: 0,
5387
+ type: "EMPTY_ERROR_MESSAGE",
5388
+ message: "Generic error message detected \u2014 use what happened + why + what to do next",
5389
+ severity: "warning"
5390
+ });
5391
+ }
5392
+ const hasDestructive = /variant\s*=\s*["']destructive["']|Delete|Remove/.test(code);
5393
+ const hasConfirm = /AlertDialog|Dialog.*confirm|confirm\s*\(|onConfirm|are you sure/i.test(code);
5394
+ if (hasDestructive && !hasConfirm) {
5395
+ issues.push({
5396
+ line: 0,
5397
+ type: "DESTRUCTIVE_NO_CONFIRM",
5398
+ message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
5399
+ severity: "warning"
5400
+ });
5401
+ }
5402
+ const hasFormSubmit = /<form\b|onSubmit|type\s*=\s*["']submit["']/.test(code);
5403
+ const hasFeedback = /toast|success|error|Saved|Saving|saving|setError|setSuccess/i.test(code);
5404
+ if (hasFormSubmit && !hasFeedback) {
5405
+ issues.push({
5406
+ line: 0,
5407
+ type: "FORM_NO_FEEDBACK",
5408
+ message: 'Form with submit but no success/error feedback pattern \u2014 add "Saving..." then "Saved" or error',
5409
+ severity: "info"
5410
+ });
5411
+ }
5412
+ const hasNav = /<nav\b|NavLink|navigation|sidebar.*link|Sidebar.*link/i.test(code);
5413
+ const hasActiveState = /pathname|active|current|aria-current|data-active/.test(code);
5414
+ if (hasNav && !hasActiveState) {
5415
+ issues.push({
5416
+ line: 0,
5417
+ type: "NAV_NO_ACTIVE_STATE",
5418
+ message: "Navigation without active/current page indicator \u2014 add active state for current route",
5419
+ severity: "info"
5420
+ });
5421
+ }
5422
+ if (validRoutes && validRoutes.length > 0) {
5423
+ const routeSet = new Set(validRoutes);
5424
+ routeSet.add("#");
5425
+ const lines = code.split("\n");
5426
+ const linkHrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
5427
+ for (let i = 0; i < lines.length; i++) {
5428
+ let match;
5429
+ while ((match = linkHrefRe.exec(lines[i])) !== null) {
5430
+ const target = match[1];
5431
+ if (target === "/" || target.startsWith("/design-system") || target.startsWith("/api") || target.startsWith("/#"))
5432
+ continue;
5433
+ if (!routeSet.has(target)) {
5434
+ issues.push({
5435
+ line: i + 1,
5436
+ type: "BROKEN_INTERNAL_LINK",
5437
+ message: `Link to "${target}" \u2014 route does not exist in project`,
5438
+ severity: "warning"
5439
+ });
5440
+ }
5615
5441
  }
5616
5442
  }
5617
5443
  }
5618
- return sections;
5444
+ return issues;
5619
5445
  }
5620
- function extractComponentUsage(code) {
5621
- const usage = {};
5622
- const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
5623
- const importedComponents = [];
5624
- let m;
5625
- while ((m = importRe.exec(code)) !== null) {
5626
- const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
5627
- importedComponents.push(...names);
5446
+ async function autoFixCode(code) {
5447
+ const fixes = [];
5448
+ let fixed = code;
5449
+ const beforeQuoteFix = fixed;
5450
+ fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
5451
+ if (fixed !== beforeQuoteFix) {
5452
+ fixes.push("fixed escaped closing quotes in strings");
5628
5453
  }
5629
- for (const comp of importedComponents) {
5630
- const re = new RegExp(`<${comp}[\\s/>]`, "g");
5631
- const matches = code.match(re);
5632
- usage[comp] = matches ? matches.length : 0;
5454
+ const beforeEntityFix = fixed;
5455
+ fixed = fixed.replace(/&lt;=/g, "<=");
5456
+ fixed = fixed.replace(/&gt;=/g, ">=");
5457
+ fixed = fixed.replace(/&amp;&amp;/g, "&&");
5458
+ fixed = fixed.replace(/([\w)\]])\s*&lt;\s*([\w(])/g, "$1 < $2");
5459
+ fixed = fixed.replace(/([\w)\]])\s*&gt;\s*([\w(])/g, "$1 > $2");
5460
+ if (fixed !== beforeEntityFix) {
5461
+ fixes.push("Fixed syntax issues");
5633
5462
  }
5634
- return usage;
5635
- }
5636
- function extractIconCount(code) {
5637
- const m = code.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
5638
- if (!m) return 0;
5639
- return m[1].split(",").map((s) => s.trim()).filter(Boolean).length;
5640
- }
5641
- function inferLayoutPattern(code) {
5642
- const funcBodyMatch = code.match(/return\s*\(\s*(<[^]*)/s);
5643
- const topLevel = funcBodyMatch ? funcBodyMatch[1].slice(0, 500) : code.slice(0, 800);
5644
- if (/grid-cols|grid\s+md:grid-cols|grid\s+lg:grid-cols/.test(topLevel)) return "grid";
5645
- if (/sidebar|aside/.test(topLevel)) return "sidebar";
5646
- if (/max-w-\d|mx-auto|container/.test(topLevel)) return "centered";
5647
- if (/min-h-screen|min-h-svh/.test(topLevel)) return "full-width";
5648
- return "unknown";
5649
- }
5650
- function detectFormUsage(code) {
5651
- const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
5652
- let m;
5653
- while ((m = importRe.exec(code)) !== null) {
5654
- const names = m[1].split(",").map((s) => s.trim());
5655
- if (names.some((n) => FORM_COMPONENTS.has(n))) return true;
5463
+ const beforeLtFix = fixed;
5464
+ fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1&lt;$2");
5465
+ fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1&lt;$2");
5466
+ if (fixed !== beforeLtFix) {
5467
+ fixes.push("escaped < in JSX text content");
5656
5468
  }
5657
- return false;
5658
- }
5659
- function summarizePageAnalysis(pageName, route, analysis) {
5660
- const parts = [`${pageName} (${route})`];
5661
- if (analysis.sections && analysis.sections.length > 0) {
5662
- parts.push(`sections: ${analysis.sections.map((s) => s.name).join(", ")}`);
5469
+ if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
5470
+ fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
5471
+ fixes.push("text-base \u2192 text-sm");
5663
5472
  }
5664
- if (analysis.componentUsage) {
5665
- const entries = Object.entries(analysis.componentUsage).filter(([, c]) => c > 0);
5666
- if (entries.length > 0) {
5667
- parts.push(`uses: ${entries.map(([n, c]) => `${n}(${c})`).join(", ")}`);
5668
- }
5473
+ if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
5474
+ fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
5475
+ fixes.push("large text in CardTitle \u2192 removed");
5669
5476
  }
5670
- if (analysis.layoutPattern && analysis.layoutPattern !== "unknown") {
5671
- parts.push(`layout: ${analysis.layoutPattern}`);
5477
+ if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
5478
+ fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
5479
+ fixes.push("heavy shadow \u2192 shadow-sm");
5672
5480
  }
5673
- if (analysis.hasForm) parts.push("has-form");
5674
- return `- ${parts.join(". ")}`;
5675
- }
5481
+ const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
5482
+ const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
5483
+ fixed
5484
+ );
5485
+ const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
5486
+ if ((hasHooks || hasEvents) && !hasUseClient) {
5487
+ fixed = `'use client'
5676
5488
 
5677
- // src/utils/concurrency.ts
5678
- async function pMap(items, fn, concurrency = 3) {
5679
- const results = new Array(items.length);
5680
- let nextIndex = 0;
5681
- async function worker() {
5682
- while (nextIndex < items.length) {
5683
- const i = nextIndex++;
5684
- results[i] = await fn(items[i], i);
5685
- }
5489
+ ${fixed}`;
5490
+ fixes.push('added "use client" (client features detected)');
5686
5491
  }
5687
- const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
5688
- await Promise.all(workers);
5689
- return results;
5690
- }
5691
-
5692
- // src/commands/chat/split-generator.ts
5693
- function buildExistingPagesContext(config2) {
5694
- const pages = config2.pages || [];
5695
- const analyzed = pages.filter((p) => p.pageAnalysis);
5696
- if (analyzed.length === 0) return "";
5697
- const lines = analyzed.map((p) => {
5698
- return summarizePageAnalysis(p.name || p.id, p.route, p.pageAnalysis);
5699
- });
5700
- let ctx = `EXISTING PAGES CONTEXT:
5701
- ${lines.join("\n")}
5702
-
5703
- Use consistent component choices, spacing, and layout patterns across all pages. Match the style and structure of existing pages.`;
5704
- const sp = config2.stylePatterns;
5705
- if (sp && typeof sp === "object") {
5706
- const parts = [];
5707
- if (sp.card) parts.push(`Cards: ${sp.card}`);
5708
- if (sp.section) parts.push(`Sections: ${sp.section}`);
5709
- if (sp.terminal) parts.push(`Terminal blocks: ${sp.terminal}`);
5710
- if (sp.iconContainer) parts.push(`Icon containers: ${sp.iconContainer}`);
5711
- if (sp.heroHeadline) parts.push(`Hero headline: ${sp.heroHeadline}`);
5712
- if (sp.sectionTitle) parts.push(`Section title: ${sp.sectionTitle}`);
5713
- if (parts.length > 0) {
5714
- ctx += `
5715
-
5716
- PROJECT STYLE PATTERNS (from sync \u2014 match these exactly):
5717
- ${parts.join("\n")}`;
5492
+ if (/^['"]use client['"]/.test(fixed.trim()) && /\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/.test(fixed)) {
5493
+ const metaMatch = fixed.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
5494
+ if (metaMatch) {
5495
+ const start = fixed.indexOf(metaMatch[0]);
5496
+ const open = fixed.indexOf("{", start);
5497
+ let depth = 1, i = open + 1;
5498
+ while (i < fixed.length && depth > 0) {
5499
+ if (fixed[i] === "{") depth++;
5500
+ else if (fixed[i] === "}") depth--;
5501
+ i++;
5502
+ }
5503
+ const tail = fixed.slice(i);
5504
+ const semi = tail.match(/^\s*;/);
5505
+ const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
5506
+ fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
5507
+ fixes.push('removed metadata export (conflicts with "use client")');
5718
5508
  }
5719
5509
  }
5720
- return ctx;
5721
- }
5722
- function extractStyleContext(pageCode) {
5723
- const unique = (arr) => [...new Set(arr)];
5724
- 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"));
5725
- const sectionSpacing = unique(pageCode.match(/py-\d+(?:\s+md:py-\d+)?/g) || []);
5726
- const headingStyles = unique(pageCode.match(/text-(?:\d*xl|lg)\s+font-(?:bold|semibold|medium)/g) || []);
5727
- const colorPatterns = unique(
5728
- (pageCode.match(
5729
- /(?:text|bg|border)-(?:primary|secondary|muted|accent|card|destructive|foreground|background)\S*/g
5730
- ) || []).concat(
5731
- pageCode.match(
5732
- /(?:text|bg|border)-(?:emerald|blue|violet|rose|amber|zinc|slate|gray|green|red|orange|indigo|purple|teal|cyan)\S*/g
5733
- ) || []
5734
- )
5735
- );
5736
- const iconPatterns = unique(pageCode.match(/(?:rounded-\S+\s+)?p-\d+(?:\.\d+)?\s*(?:bg-\S+)?/g) || []).filter(
5737
- (p) => p.includes("bg-") || p.includes("rounded")
5738
- );
5739
- const buttonPatterns = unique(
5740
- (pageCode.match(/className="[^"]*(?:hover:|active:)[^"]*"/g) || []).map((m) => m.replace(/className="|"/g, "")).filter((c) => c.includes("px-") || c.includes("py-") || c.includes("rounded"))
5741
- );
5742
- const bgPatterns = unique(pageCode.match(/bg-(?:muted|card|background|zinc|slate|gray)\S*/g) || []);
5743
- const gapPatterns = unique(pageCode.match(/gap-\d+/g) || []);
5744
- const gridPatterns = unique(pageCode.match(/grid-cols-\d+|md:grid-cols-\d+|lg:grid-cols-\d+/g) || []);
5745
- const containerPatterns = unique(pageCode.match(/container\s+max-w-\S+|max-w-\d+xl\s+mx-auto/g) || []);
5746
- const lines = [];
5747
- if (containerPatterns.length > 0) {
5748
- lines.push(`Container (MUST match for alignment with header/footer): ${containerPatterns[0]} px-4`);
5510
+ const lines = fixed.split("\n");
5511
+ let hasReplacedButton = false;
5512
+ for (let i = 0; i < lines.length; i++) {
5513
+ if (!/<button\b/.test(lines[i])) continue;
5514
+ if (lines[i].includes("aria-label")) continue;
5515
+ if (/onClick=\{.*copy/i.test(lines[i])) continue;
5516
+ const block = lines.slice(i, i + 5).join(" ");
5517
+ if (block.includes("aria-label") || /onClick=\{.*copy/i.test(block)) continue;
5518
+ lines[i] = lines[i].replace(/<button\b/g, "<Button");
5519
+ hasReplacedButton = true;
5749
5520
  }
5750
- if (cardClasses.length > 0) lines.push(`Cards: ${unique(cardClasses).slice(0, 4).join(" | ")}`);
5751
- if (sectionSpacing.length > 0) lines.push(`Section spacing: ${sectionSpacing.join(", ")}`);
5752
- if (headingStyles.length > 0) lines.push(`Headings: ${headingStyles.join(", ")}`);
5753
- if (colorPatterns.length > 0) lines.push(`Colors: ${colorPatterns.slice(0, 15).join(", ")}`);
5754
- if (iconPatterns.length > 0) lines.push(`Icon containers: ${iconPatterns.slice(0, 4).join(" | ")}`);
5755
- if (buttonPatterns.length > 0) lines.push(`Buttons: ${buttonPatterns.slice(0, 3).join(" | ")}`);
5756
- if (bgPatterns.length > 0) lines.push(`Section backgrounds: ${bgPatterns.slice(0, 6).join(", ")}`);
5757
- if (gapPatterns.length > 0) lines.push(`Gaps: ${gapPatterns.join(", ")}`);
5758
- if (gridPatterns.length > 0) lines.push(`Grids: ${gridPatterns.join(", ")}`);
5759
- if (lines.length === 0) return "";
5760
- return `STYLE CONTEXT (match these patterns exactly for visual consistency with the Home page):
5761
- ${lines.map((l) => ` - ${l}`).join("\n")}`;
5762
- }
5763
- async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
5764
- let pageNames = [];
5765
- spinner.start("Phase 1/4 \u2014 Planning pages...");
5766
- try {
5767
- const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
5768
- const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
5769
- pageNames = pageReqs.map((r) => {
5770
- const c = r.changes;
5771
- const name = c.name || c.id || "page";
5772
- const id = c.id || name.toLowerCase().replace(/\s+/g, "-");
5773
- const route = c.route || `/${id}`;
5774
- return { name, id, route };
5521
+ if (hasReplacedButton) {
5522
+ fixed = lines.join("\n");
5523
+ fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
5524
+ return "</Button>";
5775
5525
  });
5776
- } catch {
5777
- spinner.text = "AI plan failed \u2014 extracting pages from your request...";
5778
- }
5779
- if (pageNames.length === 0) {
5780
- pageNames = extractPageNamesFromMessage(message);
5781
- }
5782
- if (pageNames.length === 0) {
5783
- spinner.fail("Could not determine pages to create");
5784
- return [];
5785
- }
5786
- pageNames = deduplicatePages(pageNames);
5787
- const hasHomePage = pageNames.some((p) => p.route === "/");
5788
- if (!hasHomePage) {
5789
- const userPages = (modCtx.config.pages || []).filter(
5790
- (p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
5791
- );
5792
- const isFreshProject = userPages.length === 0;
5793
- if (isFreshProject || impliesFullWebsite(message)) {
5794
- pageNames.unshift({ name: "Home", id: "home", route: "/" });
5526
+ const openCount = (fixed.match(/<Button\b/g) || []).length;
5527
+ const closeCount = (fixed.match(/<\/Button>/g) || []).length;
5528
+ if (closeCount > openCount) {
5529
+ let excess = closeCount - openCount;
5530
+ fixed = fixed.replace(/<\/Button>/g, (m) => {
5531
+ if (excess > 0) {
5532
+ excess--;
5533
+ return "</button>";
5534
+ }
5535
+ return m;
5536
+ });
5795
5537
  }
5796
- }
5797
- const existingRoutes = new Set((modCtx.config.pages || []).map((p) => p.route).filter(Boolean));
5798
- const inferred = inferRelatedPages(pageNames).filter((p) => !existingRoutes.has(p.route));
5799
- if (inferred.length > 0) {
5800
- pageNames.push(...inferred);
5801
- pageNames = deduplicatePages(pageNames);
5802
- }
5803
- const allRoutes = pageNames.map((p) => p.route).join(", ");
5804
- const allPagesList = pageNames.map((p) => `${p.name} (${p.route})`).join(", ");
5805
- const inferredNote = inferred.length > 0 ? ` (${inferred.length} auto-inferred)` : "";
5806
- spinner.succeed(`Phase 1/4 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
5807
- const homeIdx = pageNames.findIndex((p) => p.route === "/");
5808
- const homePage = homeIdx !== -1 ? pageNames[homeIdx] : pageNames[0];
5809
- const remainingPages = pageNames.filter((_, i) => i !== (homeIdx !== -1 ? homeIdx : 0));
5810
- spinner.start(`Phase 2/4 \u2014 Generating ${homePage.name} page (sets design direction)...`);
5811
- let homeRequest = null;
5812
- let homePageCode = "";
5813
- try {
5814
- const homeResult = await parseModification(
5815
- `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.`,
5816
- modCtx,
5817
- provider,
5818
- parseOpts
5819
- );
5820
- const codePage = homeResult.requests.find((r) => r.type === "add-page");
5821
- if (codePage) {
5822
- homeRequest = codePage;
5823
- homePageCode = codePage.changes?.pageCode || "";
5538
+ const hasButtonImport = /import\s.*\bButton\b.*from\s+['"]@\/components\/ui\/button['"]/.test(fixed);
5539
+ if (!hasButtonImport) {
5540
+ const lastImportIdx = fixed.lastIndexOf("\nimport ");
5541
+ if (lastImportIdx !== -1) {
5542
+ const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
5543
+ fixed = fixed.slice(0, lineEnd + 1) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(lineEnd + 1);
5544
+ } else {
5545
+ const insertAfter = hasUseClient ? fixed.indexOf("\n") + 1 : 0;
5546
+ fixed = fixed.slice(0, insertAfter) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(insertAfter);
5547
+ }
5824
5548
  }
5825
- } catch {
5826
- }
5827
- if (!homeRequest) {
5828
- homeRequest = {
5829
- type: "add-page",
5830
- target: "new",
5831
- changes: { id: homePage.id, name: homePage.name, route: homePage.route }
5832
- };
5833
- }
5834
- spinner.succeed(`Phase 2/4 \u2014 ${homePage.name} page generated`);
5835
- spinner.start("Phase 3/4 \u2014 Extracting design patterns...");
5836
- const styleContext = homePageCode ? extractStyleContext(homePageCode) : "";
5837
- if (styleContext) {
5838
- const lineCount = styleContext.split("\n").length - 1;
5839
- spinner.succeed(`Phase 3/4 \u2014 Extracted ${lineCount} style patterns from ${homePage.name}`);
5840
- } else {
5841
- spinner.succeed("Phase 3/4 \u2014 No style patterns extracted (Home page had no code)");
5842
- }
5843
- if (remainingPages.length === 0) {
5844
- return [homeRequest];
5549
+ fixes.push("<button> \u2192 <Button> (with import)");
5845
5550
  }
5846
- spinner.start(`Phase 4/4 \u2014 Generating ${remainingPages.length} pages in parallel...`);
5847
- 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.";
5848
- 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="#".`;
5849
- 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.';
5850
- const existingPagesContext = buildExistingPagesContext(modCtx.config);
5851
- const AI_CONCURRENCY = 3;
5852
- let phase4Done = 0;
5853
- const remainingRequests = await pMap(
5854
- remainingPages,
5855
- async ({ name, id, route }) => {
5856
- const prompt = [
5857
- `Create ONE page called "${name}" at route "${route}".`,
5858
- `Context: ${message}.`,
5859
- `Generate complete pageCode for this single page only. Do not generate other pages.`,
5860
- sharedNote,
5861
- routeNote,
5862
- alignmentNote,
5863
- existingPagesContext,
5864
- styleContext
5865
- ].filter(Boolean).join("\n\n");
5866
- try {
5867
- const result = await parseModification(prompt, modCtx, provider, parseOpts);
5868
- phase4Done++;
5869
- spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
5870
- const codePage = result.requests.find((r) => r.type === "add-page");
5871
- return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
5872
- } catch {
5873
- phase4Done++;
5874
- spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
5875
- return { type: "add-page", target: "new", changes: { id, name, route } };
5876
- }
5877
- },
5878
- AI_CONCURRENCY
5879
- );
5880
- const allRequests = [homeRequest, ...remainingRequests];
5881
- const withCode = allRequests.filter((r) => r.changes?.pageCode).length;
5882
- spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
5883
- return allRequests;
5884
- }
5885
-
5886
- // src/commands/chat/modification-handler.ts
5887
- import { resolve as resolve7 } from "path";
5888
- import { mkdir as mkdir3 } from "fs/promises";
5889
- import { dirname as dirname6 } from "path";
5890
- import chalk11 from "chalk";
5891
- import {
5892
- getTemplateForPageType,
5893
- loadManifest as loadManifest5,
5894
- saveManifest,
5895
- updateUsedIn,
5896
- findSharedComponentByIdOrName,
5897
- generateSharedComponent as generateSharedComponent3
5898
- } from "@getcoherent/core";
5899
-
5900
- // src/utils/quality-validator.ts
5901
- 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;
5902
- var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
5903
- var TEXT_BASE_RE = /\btext-base\b/g;
5904
- var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
5905
- var SM_BREAKPOINT_RE = /\bsm:/g;
5906
- var XL_BREAKPOINT_RE = /\bxl:/g;
5907
- var XXL_BREAKPOINT_RE = /\b2xl:/g;
5908
- var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
5909
- var RAW_BUTTON_RE = /<button\b/g;
5910
- var RAW_INPUT_RE = /<input\b/g;
5911
- var RAW_SELECT_RE = /<select\b/g;
5912
- var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
5913
- var NATIVE_TABLE_RE = /<table\b/g;
5914
- var PLACEHOLDER_PATTERNS = [
5915
- />\s*Lorem ipsum\b/i,
5916
- />\s*Card content\s*</i,
5917
- />\s*Your (?:text|content) here\s*</i,
5918
- />\s*Description\s*</,
5919
- />\s*Title\s*</,
5920
- /placeholder\s*text/i
5921
- ];
5922
- var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
5923
- var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
5924
- var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
5925
- var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
5926
- function isInsideCommentOrString(line, matchIndex) {
5927
- const commentIdx = line.indexOf("//");
5928
- if (commentIdx !== -1 && commentIdx < matchIndex) return true;
5929
- let inSingle = false;
5930
- let inDouble = false;
5931
- let inTemplate = false;
5932
- for (let i = 0; i < matchIndex; i++) {
5933
- const ch = line[i];
5934
- const prev = i > 0 ? line[i - 1] : "";
5935
- if (prev === "\\") continue;
5936
- if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
5937
- if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
5938
- if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
5939
- }
5940
- return inSingle || inDouble || inTemplate;
5941
- }
5942
- function checkLines(code, pattern, type, message, severity, skipCommentsAndStrings = false) {
5943
- const issues = [];
5944
- const lines = code.split("\n");
5945
- let inBlockComment = false;
5946
- for (let i = 0; i < lines.length; i++) {
5947
- const line = lines[i];
5948
- if (skipCommentsAndStrings) {
5949
- if (inBlockComment) {
5950
- const endIdx = line.indexOf("*/");
5951
- if (endIdx !== -1) {
5952
- inBlockComment = false;
5551
+ const colorMap = {
5552
+ "bg-zinc-950": "bg-background",
5553
+ "bg-zinc-900": "bg-background",
5554
+ "bg-slate-950": "bg-background",
5555
+ "bg-slate-900": "bg-background",
5556
+ "bg-gray-950": "bg-background",
5557
+ "bg-gray-900": "bg-background",
5558
+ "bg-zinc-800": "bg-muted",
5559
+ "bg-slate-800": "bg-muted",
5560
+ "bg-gray-800": "bg-muted",
5561
+ "bg-zinc-100": "bg-muted",
5562
+ "bg-slate-100": "bg-muted",
5563
+ "bg-gray-100": "bg-muted",
5564
+ "bg-white": "bg-background",
5565
+ "bg-black": "bg-background",
5566
+ "text-white": "text-foreground",
5567
+ "text-black": "text-foreground",
5568
+ "text-zinc-100": "text-foreground",
5569
+ "text-zinc-200": "text-foreground",
5570
+ "text-slate-100": "text-foreground",
5571
+ "text-gray-100": "text-foreground",
5572
+ "text-zinc-400": "text-muted-foreground",
5573
+ "text-zinc-500": "text-muted-foreground",
5574
+ "text-slate-400": "text-muted-foreground",
5575
+ "text-slate-500": "text-muted-foreground",
5576
+ "text-gray-400": "text-muted-foreground",
5577
+ "text-gray-500": "text-muted-foreground",
5578
+ "border-zinc-700": "border-border",
5579
+ "border-zinc-800": "border-border",
5580
+ "border-slate-700": "border-border",
5581
+ "border-gray-700": "border-border",
5582
+ "border-zinc-200": "border-border",
5583
+ "border-slate-200": "border-border",
5584
+ "border-gray-200": "border-border"
5585
+ };
5586
+ const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
5587
+ const isInsideTerminalBlock = (offset) => {
5588
+ const preceding = fixed.slice(Math.max(0, offset - 600), offset);
5589
+ if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
5590
+ if (!/font-mono/.test(preceding)) return false;
5591
+ const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
5592
+ const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
5593
+ return lastTerminal > lastClose;
5594
+ };
5595
+ let hadColorFix = false;
5596
+ fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
5597
+ if (isCodeContext(classes)) return fullMatch;
5598
+ if (isInsideTerminalBlock(offset)) return fullMatch;
5599
+ let result = classes;
5600
+ 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;
5601
+ result = result.replace(accentColorRe, (m, prefix, color, shade) => {
5602
+ if (colorMap[m]) {
5603
+ hadColorFix = true;
5604
+ return colorMap[m];
5605
+ }
5606
+ const n = parseInt(shade);
5607
+ const isDestructive = color === "red";
5608
+ if (prefix === "bg") {
5609
+ if (n >= 500 && n <= 700) {
5610
+ hadColorFix = true;
5611
+ return isDestructive ? "bg-destructive" : "bg-primary";
5612
+ }
5613
+ if (n >= 100 && n <= 200) {
5614
+ hadColorFix = true;
5615
+ return isDestructive ? "bg-destructive/10" : "bg-primary/10";
5616
+ }
5617
+ if (n >= 300 && n <= 400) {
5618
+ hadColorFix = true;
5619
+ return isDestructive ? "bg-destructive/20" : "bg-primary/20";
5620
+ }
5621
+ if (n >= 800) {
5622
+ hadColorFix = true;
5623
+ return "bg-muted";
5953
5624
  }
5954
- continue;
5955
5625
  }
5956
- const blockStart = line.indexOf("/*");
5957
- if (blockStart !== -1 && !line.includes("*/")) {
5958
- inBlockComment = true;
5959
- continue;
5626
+ if (prefix === "text") {
5627
+ if (n >= 400 && n <= 600) {
5628
+ hadColorFix = true;
5629
+ return isDestructive ? "text-destructive" : "text-primary";
5630
+ }
5631
+ if (n >= 100 && n <= 300) {
5632
+ hadColorFix = true;
5633
+ return "text-foreground";
5634
+ }
5635
+ if (n >= 700) {
5636
+ hadColorFix = true;
5637
+ return "text-foreground";
5638
+ }
5960
5639
  }
5961
- let m;
5962
- pattern.lastIndex = 0;
5963
- while ((m = pattern.exec(line)) !== null) {
5964
- if (!isInsideCommentOrString(line, m.index)) {
5965
- issues.push({ line: i + 1, type, message, severity });
5966
- break;
5640
+ if (prefix === "border") {
5641
+ hadColorFix = true;
5642
+ return isDestructive ? "border-destructive" : "border-primary";
5643
+ }
5644
+ return m;
5645
+ });
5646
+ const neutralColorRe = /\b(bg|text|border)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
5647
+ result = result.replace(neutralColorRe, (m, prefix, _color, shade) => {
5648
+ if (colorMap[m]) {
5649
+ hadColorFix = true;
5650
+ return colorMap[m];
5651
+ }
5652
+ const n = parseInt(shade);
5653
+ if (prefix === "bg") {
5654
+ if (n >= 800) {
5655
+ hadColorFix = true;
5656
+ return "bg-background";
5657
+ }
5658
+ if (n >= 100 && n <= 300) {
5659
+ hadColorFix = true;
5660
+ return "bg-muted";
5967
5661
  }
5968
5662
  }
5969
- } else {
5970
- pattern.lastIndex = 0;
5971
- if (pattern.test(line)) {
5972
- issues.push({ line: i + 1, type, message, severity });
5663
+ if (prefix === "text") {
5664
+ if (n >= 100 && n <= 300) {
5665
+ hadColorFix = true;
5666
+ return "text-foreground";
5667
+ }
5668
+ if (n >= 400 && n <= 600) {
5669
+ hadColorFix = true;
5670
+ return "text-muted-foreground";
5671
+ }
5672
+ }
5673
+ if (prefix === "border") {
5674
+ hadColorFix = true;
5675
+ return "border-border";
5676
+ }
5677
+ return m;
5678
+ });
5679
+ if (result !== classes) return `className="${result}"`;
5680
+ return fullMatch;
5681
+ });
5682
+ if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
5683
+ const selectRe = /<select\b[^>]*>([\s\S]*?)<\/select>/g;
5684
+ let hadSelectFix = false;
5685
+ fixed = fixed.replace(selectRe, (_match, inner) => {
5686
+ const options = [];
5687
+ const optionRe = /<option\s+value="([^"]*)"[^>]*>([^<]*)<\/option>/g;
5688
+ let optMatch;
5689
+ while ((optMatch = optionRe.exec(inner)) !== null) {
5690
+ options.push({ value: optMatch[1], label: optMatch[2] });
5691
+ }
5692
+ if (options.length === 0) return _match;
5693
+ hadSelectFix = true;
5694
+ const items = options.map((o) => ` <SelectItem value="${o.value}">${o.label}</SelectItem>`).join("\n");
5695
+ return `<Select>
5696
+ <SelectTrigger>
5697
+ <SelectValue placeholder="Select..." />
5698
+ </SelectTrigger>
5699
+ <SelectContent>
5700
+ ${items}
5701
+ </SelectContent>
5702
+ </Select>`;
5703
+ });
5704
+ if (hadSelectFix) {
5705
+ fixes.push("<select> \u2192 shadcn Select");
5706
+ const selectImport = `import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'`;
5707
+ if (!/from\s+['"]@\/components\/ui\/select['"]/.test(fixed)) {
5708
+ const replaced = fixed.replace(
5709
+ /(import\s+\{[^}]*\}\s+from\s+['"]@\/components\/ui\/[^'"]+['"])/,
5710
+ `$1
5711
+ ${selectImport}`
5712
+ );
5713
+ if (replaced !== fixed) {
5714
+ fixed = replaced;
5715
+ } else {
5716
+ fixed = selectImport + "\n" + fixed;
5973
5717
  }
5974
5718
  }
5975
5719
  }
5976
- return issues;
5977
- }
5978
- function validatePageQuality(code, validRoutes) {
5979
- const issues = [];
5980
- const allLines = code.split("\n");
5981
- const isTerminalContext = (lineNum) => {
5982
- const start = Math.max(0, lineNum - 20);
5983
- const nearby = allLines.slice(start, lineNum).join(" ");
5984
- if (/font-mono/.test(allLines[lineNum - 1] || "")) return true;
5985
- if (/bg-zinc-950|bg-zinc-900/.test(nearby) && /font-mono/.test(nearby)) return true;
5986
- return false;
5987
- };
5988
- issues.push(
5989
- ...checkLines(
5990
- code,
5991
- RAW_COLOR_RE,
5992
- "RAW_COLOR",
5993
- "Raw Tailwind color detected \u2014 use semantic tokens (bg-primary, text-muted-foreground, etc.)",
5994
- "error"
5995
- ).filter((issue) => !isTerminalContext(issue.line))
5996
- );
5997
- issues.push(
5998
- ...checkLines(
5999
- code,
6000
- HEX_IN_CLASS_RE,
6001
- "HEX_IN_CLASS",
6002
- "Hex color in className \u2014 use CSS variables via semantic tokens",
6003
- "error"
6004
- )
6005
- );
6006
- issues.push(
6007
- ...checkLines(code, TEXT_BASE_RE, "TEXT_BASE", "text-base detected \u2014 use text-sm as base font size", "warning")
6008
- );
6009
- issues.push(
6010
- ...checkLines(code, HEAVY_SHADOW_RE, "HEAVY_SHADOW", "Heavy shadow detected \u2014 use shadow-sm or none", "warning")
6011
- );
6012
- issues.push(
6013
- ...checkLines(
6014
- code,
6015
- SM_BREAKPOINT_RE,
6016
- "SM_BREAKPOINT",
6017
- "sm: breakpoint \u2014 consider if md:/lg: is sufficient",
6018
- "info"
6019
- )
6020
- );
6021
- issues.push(
6022
- ...checkLines(
6023
- code,
6024
- XL_BREAKPOINT_RE,
6025
- "XL_BREAKPOINT",
6026
- "xl: breakpoint \u2014 consider if md:/lg: is sufficient",
6027
- "info"
6028
- )
6029
- );
6030
- issues.push(
6031
- ...checkLines(
6032
- code,
6033
- XXL_BREAKPOINT_RE,
6034
- "XXL_BREAKPOINT",
6035
- "2xl: breakpoint \u2014 rarely needed, consider xl: instead",
6036
- "warning"
6037
- )
6038
- );
6039
- issues.push(
6040
- ...checkLines(
6041
- code,
6042
- LARGE_CARD_TITLE_RE,
6043
- "LARGE_CARD_TITLE",
6044
- "Large text on CardTitle \u2014 use text-sm font-medium",
6045
- "warning"
6046
- )
6047
- );
6048
- const codeLines = code.split("\n");
6049
- issues.push(
6050
- ...checkLines(
6051
- code,
6052
- RAW_BUTTON_RE,
6053
- "NATIVE_BUTTON",
6054
- "Native <button> \u2014 use Button from @/components/ui/button",
6055
- "error",
6056
- true
6057
- ).filter((issue) => {
6058
- const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
6059
- if (nearby.includes("aria-label")) return false;
6060
- if (/onClick=\{.*copy/i.test(nearby)) return false;
6061
- return true;
6062
- })
6063
- );
6064
- issues.push(
6065
- ...checkLines(
6066
- code,
6067
- RAW_SELECT_RE,
6068
- "NATIVE_SELECT",
6069
- "Native <select> \u2014 use Select from @/components/ui/select",
6070
- "error",
6071
- true
6072
- )
6073
- );
6074
- issues.push(
6075
- ...checkLines(
6076
- code,
6077
- NATIVE_CHECKBOX_RE,
6078
- "NATIVE_CHECKBOX",
6079
- 'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
6080
- "error",
6081
- true
6082
- )
6083
- );
6084
- issues.push(
6085
- ...checkLines(
6086
- code,
6087
- NATIVE_TABLE_RE,
6088
- "NATIVE_TABLE",
6089
- "Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
6090
- "warning",
6091
- true
6092
- )
6093
- );
6094
- const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
6095
- if (!hasInputImport) {
6096
- issues.push(
6097
- ...checkLines(
6098
- code,
6099
- RAW_INPUT_RE,
6100
- "RAW_INPUT",
6101
- "Raw <input> element \u2014 import and use Input from @/components/ui/input",
6102
- "warning",
6103
- true
6104
- )
6105
- );
5720
+ const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
5721
+ if (lucideImportMatch) {
5722
+ let lucideExports = null;
5723
+ try {
5724
+ const { createRequire } = await import("module");
5725
+ const require2 = createRequire(process.cwd() + "/package.json");
5726
+ const lr = require2("lucide-react");
5727
+ lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
5728
+ } catch {
5729
+ }
5730
+ if (lucideExports) {
5731
+ const nonLucideImports = /* @__PURE__ */ new Set();
5732
+ for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from\s*["'](?!lucide-react)([^"']+)["']/g)) {
5733
+ m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => nonLucideImports.add(n));
5734
+ }
5735
+ const iconNames = lucideImportMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
5736
+ const duplicates = iconNames.filter((name) => nonLucideImports.has(name));
5737
+ let newImport = lucideImportMatch[1];
5738
+ for (const dup of duplicates) {
5739
+ newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
5740
+ fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
5741
+ }
5742
+ const invalid = iconNames.filter((name) => !lucideExports.has(name) && !nonLucideImports.has(name));
5743
+ if (invalid.length > 0) {
5744
+ const fallback = "Circle";
5745
+ for (const bad of invalid) {
5746
+ const re = new RegExp(`\\b${bad}\\b`, "g");
5747
+ newImport = newImport.replace(re, fallback);
5748
+ fixed = fixed.replace(re, fallback);
5749
+ }
5750
+ fixes.push(`invalid lucide icons \u2192 ${fallback}: ${invalid.join(", ")}`);
5751
+ }
5752
+ if (duplicates.length > 0 || invalid.length > 0) {
5753
+ const importedNames = [
5754
+ ...new Set(
5755
+ newImport.split(",").map((s) => s.trim()).filter(Boolean)
5756
+ )
5757
+ ];
5758
+ const originalImportLine = lucideImportMatch[0];
5759
+ fixed = fixed.replace(originalImportLine, `import { ${importedNames.join(", ")} } from "lucide-react"`);
5760
+ }
5761
+ }
6106
5762
  }
6107
- for (const pattern of PLACEHOLDER_PATTERNS) {
6108
- const lines = code.split("\n");
6109
- for (let i = 0; i < lines.length; i++) {
6110
- if (pattern.test(lines[i])) {
6111
- issues.push({
6112
- line: i + 1,
6113
- type: "PLACEHOLDER",
6114
- message: "Placeholder content detected \u2014 use real contextual content",
6115
- severity: "error"
6116
- });
5763
+ const lucideImportMatch2 = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
5764
+ if (lucideImportMatch2) {
5765
+ let lucideExports2 = null;
5766
+ try {
5767
+ const { createRequire } = await import("module");
5768
+ const req = createRequire(process.cwd() + "/package.json");
5769
+ const lr = req("lucide-react");
5770
+ lucideExports2 = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
5771
+ } catch {
5772
+ }
5773
+ if (lucideExports2) {
5774
+ const allImportedNames = /* @__PURE__ */ new Set();
5775
+ for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
5776
+ m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => allImportedNames.add(n));
5777
+ }
5778
+ for (const m of fixed.matchAll(/import\s+([A-Z]\w+)\s+from/g)) {
5779
+ allImportedNames.add(m[1]);
5780
+ }
5781
+ const lucideImported = new Set(
5782
+ lucideImportMatch2[1].split(",").map((s) => s.trim()).filter(Boolean)
5783
+ );
5784
+ const jsxIconRefs = [...new Set([...fixed.matchAll(/<([A-Z][a-zA-Z]*Icon)\s/g)].map((m) => m[1]))];
5785
+ const missing = [];
5786
+ for (const ref of jsxIconRefs) {
5787
+ if (allImportedNames.has(ref)) continue;
5788
+ if (fixed.includes(`function ${ref}`) || fixed.includes(`const ${ref}`)) continue;
5789
+ const baseName = ref.replace(/Icon$/, "");
5790
+ if (lucideExports2.has(ref)) {
5791
+ missing.push(ref);
5792
+ lucideImported.add(ref);
5793
+ } else if (lucideExports2.has(baseName)) {
5794
+ const re = new RegExp(`\\b${ref}\\b`, "g");
5795
+ fixed = fixed.replace(re, baseName);
5796
+ missing.push(baseName);
5797
+ lucideImported.add(baseName);
5798
+ fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
5799
+ } else {
5800
+ const fallback = "Circle";
5801
+ const re = new RegExp(`\\b${ref}\\b`, "g");
5802
+ fixed = fixed.replace(re, fallback);
5803
+ lucideImported.add(fallback);
5804
+ fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
5805
+ }
5806
+ }
5807
+ if (missing.length > 0) {
5808
+ const allNames = [...lucideImported];
5809
+ const origLine = lucideImportMatch2[0];
5810
+ fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
5811
+ fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
6117
5812
  }
6118
5813
  }
6119
5814
  }
6120
- const hasGrid = /\bgrid\b/.test(code);
6121
- const hasResponsive = /\bmd:|lg:/.test(code);
6122
- if (hasGrid && !hasResponsive) {
5815
+ fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
5816
+ const cleaned = inner.replace(/\s{2,}/g, " ").trim();
5817
+ return `className="${cleaned}"`;
5818
+ });
5819
+ let imgCounter = 1;
5820
+ const beforeImgFix = fixed;
5821
+ fixed = fixed.replace(/["']\/api\/placeholder\/(\d+)\/(\d+)["']/g, (_m, w, h) => {
5822
+ return `"https://picsum.photos/${w}/${h}?random=${imgCounter++}"`;
5823
+ });
5824
+ fixed = fixed.replace(/["']\/placeholder-avatar[^"']*["']/g, () => {
5825
+ return `"https://i.pravatar.cc/150?u=user${imgCounter++}"`;
5826
+ });
5827
+ fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
5828
+ const height = h || w;
5829
+ return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
5830
+ });
5831
+ fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
5832
+ return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
5833
+ });
5834
+ fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
5835
+ return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
5836
+ });
5837
+ if (fixed !== beforeImgFix) {
5838
+ fixes.push("placeholder images \u2192 working URLs (picsum/pravatar)");
5839
+ }
5840
+ return { code: fixed, fixes };
5841
+ }
5842
+ function formatIssues(issues) {
5843
+ if (issues.length === 0) return "";
5844
+ const errors = issues.filter((i) => i.severity === "error");
5845
+ const warnings = issues.filter((i) => i.severity === "warning");
5846
+ const infos = issues.filter((i) => i.severity === "info");
5847
+ const lines = [];
5848
+ if (errors.length > 0) {
5849
+ lines.push(` \u274C ${errors.length} error(s):`);
5850
+ for (const e of errors) {
5851
+ lines.push(` L${e.line}: [${e.type}] ${e.message}`);
5852
+ }
5853
+ }
5854
+ if (warnings.length > 0) {
5855
+ lines.push(` \u26A0\uFE0F ${warnings.length} warning(s):`);
5856
+ for (const w of warnings) {
5857
+ lines.push(` L${w.line}: [${w.type}] ${w.message}`);
5858
+ }
5859
+ }
5860
+ if (infos.length > 0) {
5861
+ lines.push(` \u2139\uFE0F ${infos.length} info:`);
5862
+ for (const i of infos) {
5863
+ lines.push(` L${i.line}: [${i.type}] ${i.message}`);
5864
+ }
5865
+ }
5866
+ return lines.join("\n");
5867
+ }
5868
+ function checkDesignConsistency(code) {
5869
+ const warnings = [];
5870
+ const hexPattern = /\[#[0-9a-fA-F]{3,8}\]/g;
5871
+ for (const match of code.matchAll(hexPattern)) {
5872
+ warnings.push({
5873
+ type: "hardcoded-color",
5874
+ message: `Hardcoded color ${match[0]} \u2014 use a design token (e.g., bg-primary) instead`
5875
+ });
5876
+ }
5877
+ const spacingPattern = /[pm][trblxy]?-\[\d+px\]/g;
5878
+ for (const match of code.matchAll(spacingPattern)) {
5879
+ warnings.push({
5880
+ type: "arbitrary-spacing",
5881
+ message: `Arbitrary spacing ${match[0]} \u2014 use Tailwind spacing scale instead`
5882
+ });
5883
+ }
5884
+ return warnings;
5885
+ }
5886
+ function verifyIncrementalEdit(before, after) {
5887
+ const issues = [];
5888
+ const hookPattern = /\buse[A-Z]\w+\s*\(/;
5889
+ if (hookPattern.test(after) && !after.includes("'use client'") && !after.includes('"use client"')) {
6123
5890
  issues.push({
6124
- line: 0,
6125
- type: "NO_RESPONSIVE",
6126
- message: "Grid layout without responsive breakpoints (md: or lg:)",
6127
- severity: "warning"
5891
+ type: "missing-use-client",
5892
+ message: 'Code uses React hooks but missing "use client" directive'
6128
5893
  });
6129
5894
  }
6130
- issues.push(
6131
- ...checkLines(
6132
- code,
6133
- IMG_WITHOUT_ALT_RE,
6134
- "MISSING_ALT",
6135
- '<img> without alt attribute \u2014 add descriptive alt or alt="" for decorative images',
6136
- "error"
6137
- )
6138
- );
6139
- issues.push(
6140
- ...checkLines(
6141
- code,
6142
- GENERIC_BUTTON_LABELS,
6143
- "GENERIC_BUTTON_TEXT",
6144
- 'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
6145
- "warning"
6146
- )
6147
- );
6148
- const h1Matches = code.match(/<h1[\s>]/g);
6149
- if (!h1Matches || h1Matches.length === 0) {
6150
- issues.push({
6151
- line: 0,
6152
- type: "NO_H1",
6153
- message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
6154
- severity: "warning"
6155
- });
6156
- } else if (h1Matches.length > 1) {
5895
+ if (!after.includes("export default")) {
6157
5896
  issues.push({
6158
- line: 0,
6159
- type: "MULTIPLE_H1",
6160
- message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
6161
- severity: "warning"
5897
+ type: "missing-default-export",
5898
+ message: "Missing default export \u2014 page component must have a default export"
6162
5899
  });
6163
5900
  }
6164
- const headingLevels = [...code.matchAll(/<h([1-6])[\s>]/g)].map((m) => parseInt(m[1]));
6165
- for (let i = 1; i < headingLevels.length; i++) {
6166
- if (headingLevels[i] > headingLevels[i - 1] + 1) {
6167
- issues.push({
6168
- line: 0,
6169
- type: "SKIPPED_HEADING",
6170
- message: `Heading level skipped: h${headingLevels[i - 1]} \u2192 h${headingLevels[i]} \u2014 don't skip levels`,
6171
- severity: "warning"
6172
- });
6173
- break;
6174
- }
6175
- }
6176
- const hasLabelImport = /import\s.*Label.*from\s+['"]@\/components\/ui\//.test(code);
6177
- const inputCount = (code.match(INPUT_TAG_RE) || []).length;
6178
- const labelForCount = (code.match(LABEL_FOR_RE) || []).length;
6179
- if (hasLabelImport && inputCount > 0 && labelForCount === 0) {
6180
- issues.push({
6181
- line: 0,
6182
- type: "MISSING_LABEL",
6183
- message: "Inputs found but no Label with htmlFor \u2014 every input must have a visible label",
6184
- severity: "error"
6185
- });
5901
+ const importRegex = /import\s+\{([^}]+)\}\s+from/g;
5902
+ const beforeImports = /* @__PURE__ */ new Set();
5903
+ const afterImports = /* @__PURE__ */ new Set();
5904
+ for (const match of before.matchAll(importRegex)) {
5905
+ match[1].split(",").forEach((s) => beforeImports.add(s.trim()));
6186
5906
  }
6187
- if (!hasLabelImport && inputCount > 0 && !/<label\b/i.test(code)) {
6188
- issues.push({
6189
- line: 0,
6190
- type: "MISSING_LABEL",
6191
- message: "Inputs found but no Label component \u2014 import Label and add htmlFor on each input",
6192
- severity: "error"
6193
- });
5907
+ for (const match of after.matchAll(importRegex)) {
5908
+ match[1].split(",").forEach((s) => afterImports.add(s.trim()));
6194
5909
  }
6195
- const hasPlaceholder = /placeholder\s*=/.test(code);
6196
- if (hasPlaceholder && inputCount > 0 && labelForCount === 0 && !/<label\b/i.test(code) && !/<Label\b/.test(code)) {
6197
- issues.push({
6198
- line: 0,
6199
- type: "PLACEHOLDER_ONLY_LABEL",
6200
- message: "Inputs use placeholder only \u2014 add visible Label with htmlFor (placeholder is not a substitute)",
6201
- severity: "error"
6202
- });
5910
+ for (const symbol of beforeImports) {
5911
+ if (!afterImports.has(symbol) && symbol.length > 0) {
5912
+ const codeWithoutImports = after.replace(/^import\s+.*$/gm, "");
5913
+ const symbolRegex = new RegExp(`\\b${symbol}\\b`);
5914
+ if (symbolRegex.test(codeWithoutImports)) {
5915
+ issues.push({
5916
+ type: "missing-import",
5917
+ symbol,
5918
+ message: `Import for "${symbol}" was removed but symbol is still used in code`
5919
+ });
5920
+ }
5921
+ }
6203
5922
  }
6204
- const hasInteractive = /<Button\b|<button\b|<a\b/.test(code);
6205
- const hasFocusVisible = /focus-visible:/.test(code);
6206
- const usesShadcnButton = /import\s.*Button.*from\s+['"]@\/components\/ui\//.test(code);
6207
- if (hasInteractive && !hasFocusVisible && !usesShadcnButton) {
6208
- issues.push({
6209
- line: 0,
6210
- type: "MISSING_FOCUS_VISIBLE",
6211
- message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
6212
- severity: "info"
6213
- });
5923
+ return issues;
5924
+ }
5925
+
5926
+ // src/commands/chat/utils.ts
5927
+ import { resolve as resolve5 } from "path";
5928
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
5929
+ import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
5930
+ import chalk8 from "chalk";
5931
+ var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
5932
+ function isMarketingRoute(route) {
5933
+ const slug = route.replace(/^\//, "").split("/")[0] || "";
5934
+ return MARKETING_ROUTES.has(slug);
5935
+ }
5936
+ function routeToFsPath(projectRoot, route, isAuth) {
5937
+ const slug = route.replace(/^\//, "");
5938
+ if (isAuth) {
5939
+ return resolve5(projectRoot, "app", "(auth)", slug || "login", "page.tsx");
6214
5940
  }
6215
- const hasTableOrList = /<Table\b|<table\b|\.map\s*\(|<ul\b|<ol\b/.test(code);
6216
- 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(
6217
- code
6218
- );
6219
- if (hasTableOrList && !hasEmptyCheck) {
6220
- issues.push({
6221
- line: 0,
6222
- type: "NO_EMPTY_STATE",
6223
- message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
6224
- severity: "warning"
6225
- });
5941
+ if (!slug) {
5942
+ return resolve5(projectRoot, "app", "page.tsx");
6226
5943
  }
6227
- const hasDataFetching = /fetch\s*\(|useQuery|useSWR|useEffect\s*\([^)]*fetch|getData|loadData/i.test(code);
6228
- const hasLoadingPattern = /skeleton|Skeleton|spinner|Spinner|isLoading|loading|Loading/.test(code);
6229
- if (hasDataFetching && !hasLoadingPattern) {
6230
- issues.push({
6231
- line: 0,
6232
- type: "NO_LOADING_STATE",
6233
- message: "Page with data fetching but no loading/skeleton pattern \u2014 add skeleton or spinner",
6234
- severity: "warning"
6235
- });
5944
+ if (isMarketingRoute(route)) {
5945
+ return resolve5(projectRoot, "app", slug, "page.tsx");
6236
5946
  }
6237
- const hasGenericError = /Something went wrong|"Error"|'Error'|>Error<\//.test(code) || /error\.message\s*\|\|\s*["']Error["']/.test(code);
6238
- if (hasGenericError) {
6239
- issues.push({
6240
- line: 0,
6241
- type: "EMPTY_ERROR_MESSAGE",
6242
- message: "Generic error message detected \u2014 use what happened + why + what to do next",
6243
- severity: "warning"
6244
- });
5947
+ return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
5948
+ }
5949
+ function routeToRelPath(route, isAuth) {
5950
+ const slug = route.replace(/^\//, "");
5951
+ if (isAuth) {
5952
+ return `app/(auth)/${slug || "login"}/page.tsx`;
6245
5953
  }
6246
- const hasDestructive = /variant\s*=\s*["']destructive["']|Delete|Remove/.test(code);
6247
- const hasConfirm = /AlertDialog|Dialog.*confirm|confirm\s*\(|onConfirm|are you sure/i.test(code);
6248
- if (hasDestructive && !hasConfirm) {
6249
- issues.push({
6250
- line: 0,
6251
- type: "DESTRUCTIVE_NO_CONFIRM",
6252
- message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
6253
- severity: "warning"
6254
- });
5954
+ if (!slug) {
5955
+ return "app/page.tsx";
6255
5956
  }
6256
- const hasFormSubmit = /<form\b|onSubmit|type\s*=\s*["']submit["']/.test(code);
6257
- const hasFeedback = /toast|success|error|Saved|Saving|saving|setError|setSuccess/i.test(code);
6258
- if (hasFormSubmit && !hasFeedback) {
6259
- issues.push({
6260
- line: 0,
6261
- type: "FORM_NO_FEEDBACK",
6262
- message: 'Form with submit but no success/error feedback pattern \u2014 add "Saving..." then "Saved" or error',
6263
- severity: "info"
6264
- });
5957
+ if (isMarketingRoute(route)) {
5958
+ return `app/${slug}/page.tsx`;
6265
5959
  }
6266
- const hasNav = /<nav\b|NavLink|navigation|sidebar.*link|Sidebar.*link/i.test(code);
6267
- const hasActiveState = /pathname|active|current|aria-current|data-active/.test(code);
6268
- if (hasNav && !hasActiveState) {
6269
- issues.push({
6270
- line: 0,
6271
- type: "NAV_NO_ACTIVE_STATE",
6272
- message: "Navigation without active/current page indicator \u2014 add active state for current route",
6273
- severity: "info"
6274
- });
5960
+ return `app/(app)/${slug}/page.tsx`;
5961
+ }
5962
+ function deduplicatePages(pages) {
5963
+ const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
5964
+ const seen = /* @__PURE__ */ new Map();
5965
+ return pages.filter((page, idx) => {
5966
+ const norm = normalize(page.route);
5967
+ if (seen.has(norm)) return false;
5968
+ seen.set(norm, idx);
5969
+ return true;
5970
+ });
5971
+ }
5972
+ function extractComponentIdsFromCode(code) {
5973
+ const ids = /* @__PURE__ */ new Set();
5974
+ const allMatches = code.matchAll(/@\/components\/((?:ui\/)?[a-z0-9-]+)/g);
5975
+ for (const m of allMatches) {
5976
+ if (!m[1]) continue;
5977
+ let id = m[1];
5978
+ if (id.startsWith("ui/")) id = id.slice(3);
5979
+ if (id === "shared" || id.startsWith("shared/")) continue;
5980
+ if (id) ids.add(id);
6275
5981
  }
6276
- if (validRoutes && validRoutes.length > 0) {
6277
- const routeSet = new Set(validRoutes);
6278
- routeSet.add("#");
6279
- const lines = code.split("\n");
6280
- const linkHrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
6281
- for (let i = 0; i < lines.length; i++) {
6282
- let match;
6283
- while ((match = linkHrefRe.exec(lines[i])) !== null) {
6284
- const target = match[1];
6285
- if (target === "/" || target.startsWith("/design-system") || target.startsWith("/api") || target.startsWith("/#"))
6286
- continue;
6287
- if (!routeSet.has(target)) {
6288
- issues.push({
6289
- line: i + 1,
6290
- type: "BROKEN_INTERNAL_LINK",
6291
- message: `Link to "${target}" \u2014 route does not exist in project`,
6292
- severity: "warning"
6293
- });
6294
- }
5982
+ return ids;
5983
+ }
5984
+ async function warnInlineDuplicates(projectRoot, pageName, pageCode, manifest) {
5985
+ const sectionOrWidget = manifest.shared.filter((e) => e.type === "section" || e.type === "widget");
5986
+ if (sectionOrWidget.length === 0) return;
5987
+ for (const e of sectionOrWidget) {
5988
+ const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
5989
+ const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
5990
+ if (hasImport) continue;
5991
+ const sameNameAsTag = new RegExp(`<\\/?${e.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s>]`).test(pageCode);
5992
+ if (sameNameAsTag) {
5993
+ console.log(
5994
+ chalk8.yellow(
5995
+ `
5996
+ \u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
5997
+ )
5998
+ );
5999
+ continue;
6000
+ }
6001
+ try {
6002
+ const fullPath = resolve5(projectRoot, e.file);
6003
+ const sharedSnippet = (await readFile(fullPath)).slice(0, 600);
6004
+ const sharedTokens = new Set(sharedSnippet.match(/\b[a-zA-Z0-9-]{4,}\b/g) ?? []);
6005
+ const pageTokens = pageCode.match(/\b[a-zA-Z0-9-]+\b/g) ?? [];
6006
+ let overlap = 0;
6007
+ for (const t of sharedTokens) {
6008
+ if (pageTokens.includes(t)) overlap++;
6009
+ }
6010
+ if (overlap >= 12 && sharedTokens.size >= 10) {
6011
+ console.log(
6012
+ chalk8.yellow(
6013
+ `
6014
+ \u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
6015
+ )
6016
+ );
6295
6017
  }
6018
+ } catch {
6296
6019
  }
6297
6020
  }
6298
- return issues;
6299
6021
  }
6300
- async function autoFixCode(code) {
6301
- const fixes = [];
6302
- let fixed = code;
6303
- const beforeQuoteFix = fixed;
6304
- fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
6305
- if (fixed !== beforeQuoteFix) {
6306
- fixes.push("fixed escaped closing quotes in strings");
6022
+ async function loadConfig(configPath) {
6023
+ if (!existsSync13(configPath)) {
6024
+ throw new Error(
6025
+ `Design system config not found at ${configPath}
6026
+ Run "coherent init" first to create a project.`
6027
+ );
6307
6028
  }
6308
- const beforeEntityFix = fixed;
6309
- fixed = fixed.replace(/&lt;=/g, "<=");
6310
- fixed = fixed.replace(/&gt;=/g, ">=");
6311
- fixed = fixed.replace(/&amp;&amp;/g, "&&");
6312
- fixed = fixed.replace(/([\w)\]])\s*&lt;\s*([\w(])/g, "$1 < $2");
6313
- fixed = fixed.replace(/([\w)\]])\s*&gt;\s*([\w(])/g, "$1 > $2");
6314
- if (fixed !== beforeEntityFix) {
6315
- fixes.push("Fixed syntax issues");
6316
- }
6317
- const beforeLtFix = fixed;
6318
- fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1&lt;$2");
6319
- fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1&lt;$2");
6320
- if (fixed !== beforeLtFix) {
6321
- fixes.push("escaped < in JSX text content");
6029
+ const manager = new DesignSystemManager3(configPath);
6030
+ await manager.load();
6031
+ return manager.getConfig();
6032
+ }
6033
+ function requireProject() {
6034
+ const project = findConfig();
6035
+ if (!project) {
6036
+ exitNotCoherent();
6322
6037
  }
6323
- if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
6324
- fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
6325
- fixes.push("text-base \u2192 text-sm");
6038
+ warnIfVolatile(project.root);
6039
+ return project;
6040
+ }
6041
+ async function resolveTargetFlags(message, options, config2, projectRoot) {
6042
+ if (options.component) {
6043
+ const manifest = await loadManifest4(projectRoot);
6044
+ const target = options.component;
6045
+ const entry = manifest.shared.find(
6046
+ (s) => s.name.toLowerCase() === target.toLowerCase() || s.id.toLowerCase() === target.toLowerCase()
6047
+ );
6048
+ if (entry) {
6049
+ const filePath = resolve5(projectRoot, entry.file);
6050
+ let currentCode = "";
6051
+ if (existsSync13(filePath)) {
6052
+ currentCode = readFileSync8(filePath, "utf-8");
6053
+ }
6054
+ const codeSnippet = currentCode ? `
6055
+
6056
+ Current code of ${entry.name}:
6057
+ \`\`\`tsx
6058
+ ${currentCode}
6059
+ \`\`\`` : "";
6060
+ 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}`;
6061
+ }
6062
+ console.log(chalk8.yellow(`
6063
+ \u26A0\uFE0F Component "${target}" not found in shared components.`));
6064
+ console.log(chalk8.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
6065
+ console.log(chalk8.dim(" Proceeding with message as-is...\n"));
6326
6066
  }
6327
- if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
6328
- fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
6329
- fixes.push("large text in CardTitle \u2192 removed");
6067
+ if (options.page) {
6068
+ const target = options.page;
6069
+ const page = config2.pages.find(
6070
+ (p) => p.name.toLowerCase() === target.toLowerCase() || p.id.toLowerCase() === target.toLowerCase() || p.route === target || p.route === "/" + target
6071
+ );
6072
+ if (page) {
6073
+ const relPath = page.route === "/" ? "app/page.tsx" : `app${page.route}/page.tsx`;
6074
+ const filePath = resolve5(projectRoot, relPath);
6075
+ let currentCode = "";
6076
+ if (existsSync13(filePath)) {
6077
+ currentCode = readFileSync8(filePath, "utf-8");
6078
+ }
6079
+ const codeSnippet = currentCode ? `
6080
+
6081
+ Current code of ${page.name} page:
6082
+ \`\`\`tsx
6083
+ ${currentCode}
6084
+ \`\`\`` : "";
6085
+ 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}`;
6086
+ }
6087
+ console.log(chalk8.yellow(`
6088
+ \u26A0\uFE0F Page "${target}" not found.`));
6089
+ console.log(chalk8.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
6090
+ console.log(chalk8.dim(" Proceeding with message as-is...\n"));
6330
6091
  }
6331
- if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
6332
- fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
6333
- fixes.push("heavy shadow \u2192 shadow-sm");
6092
+ if (options.token) {
6093
+ const target = options.token;
6094
+ return `Change design token "${target}": ${message}. Update the token value in design-system.config.ts and ensure globals.css reflects the change.`;
6334
6095
  }
6335
- const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
6336
- const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
6337
- fixed
6338
- );
6339
- const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
6340
- if ((hasHooks || hasEvents) && !hasUseClient) {
6341
- fixed = `'use client'
6096
+ return message;
6097
+ }
6342
6098
 
6343
- ${fixed}`;
6344
- fixes.push('added "use client" (client features detected)');
6099
+ // src/commands/chat/request-parser.ts
6100
+ var AUTH_FLOW_PATTERNS = {
6101
+ "/login": ["/register", "/forgot-password"],
6102
+ "/signin": ["/register", "/forgot-password"],
6103
+ "/signup": ["/login"],
6104
+ "/register": ["/login"],
6105
+ "/forgot-password": ["/login", "/reset-password"],
6106
+ "/reset-password": ["/login"]
6107
+ };
6108
+ var PAGE_RELATIONSHIP_RULES = [
6109
+ {
6110
+ trigger: /\/(products|catalog|marketplace|listings|shop|store)\b/i,
6111
+ related: [{ id: "product-detail", name: "Product Detail", route: "/products/[id]" }]
6112
+ },
6113
+ {
6114
+ trigger: /\/(blog|news|articles|posts)\b/i,
6115
+ related: [{ id: "article-detail", name: "Article", route: "/blog/[slug]" }]
6116
+ },
6117
+ {
6118
+ trigger: /\/(campaigns|ads|ad-campaigns)\b/i,
6119
+ related: [{ id: "campaign-detail", name: "Campaign Detail", route: "/campaigns/[id]" }]
6120
+ },
6121
+ {
6122
+ trigger: /\/(dashboard|admin)\b/i,
6123
+ related: [{ id: "settings", name: "Settings", route: "/settings" }]
6124
+ },
6125
+ {
6126
+ trigger: /\/pricing\b/i,
6127
+ related: [{ id: "checkout", name: "Checkout", route: "/checkout" }]
6345
6128
  }
6346
- if (/^['"]use client['"]/.test(fixed.trim()) && /\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/.test(fixed)) {
6347
- const metaMatch = fixed.match(/\bexport\s+const\s+metadata\s*:\s*Metadata\s*=\s*\{/);
6348
- if (metaMatch) {
6349
- const start = fixed.indexOf(metaMatch[0]);
6350
- const open = fixed.indexOf("{", start);
6351
- let depth = 1, i = open + 1;
6352
- while (i < fixed.length && depth > 0) {
6353
- if (fixed[i] === "{") depth++;
6354
- else if (fixed[i] === "}") depth--;
6355
- i++;
6129
+ ];
6130
+ function extractInternalLinks(code) {
6131
+ const links = /* @__PURE__ */ new Set();
6132
+ const hrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
6133
+ let m;
6134
+ while ((m = hrefRe.exec(code)) !== null) {
6135
+ const route = m[1];
6136
+ if (route === "/" || route.startsWith("/design-system") || route.startsWith("/#") || route.startsWith("/api"))
6137
+ continue;
6138
+ links.add(route);
6139
+ }
6140
+ return [...links];
6141
+ }
6142
+ function inferRelatedPages(plannedPages) {
6143
+ const plannedRoutes = new Set(plannedPages.map((p) => p.route));
6144
+ const inferred = [];
6145
+ for (const { route } of plannedPages) {
6146
+ const authRelated = AUTH_FLOW_PATTERNS[route];
6147
+ if (authRelated) {
6148
+ for (const rel of authRelated) {
6149
+ if (!plannedRoutes.has(rel)) {
6150
+ const slug = rel.slice(1);
6151
+ const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
6152
+ inferred.push({ id: slug, name, route: rel });
6153
+ plannedRoutes.add(rel);
6154
+ }
6356
6155
  }
6357
- const tail = fixed.slice(i);
6358
- const semi = tail.match(/^\s*;/);
6359
- const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
6360
- fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
6361
- fixes.push('removed metadata export (conflicts with "use client")');
6362
6156
  }
6363
- }
6364
- const lines = fixed.split("\n");
6365
- let hasReplacedButton = false;
6366
- for (let i = 0; i < lines.length; i++) {
6367
- if (!/<button\b/.test(lines[i])) continue;
6368
- if (lines[i].includes("aria-label")) continue;
6369
- if (/onClick=\{.*copy/i.test(lines[i])) continue;
6370
- const block = lines.slice(i, i + 5).join(" ");
6371
- if (block.includes("aria-label") || /onClick=\{.*copy/i.test(block)) continue;
6372
- lines[i] = lines[i].replace(/<button\b/g, "<Button");
6373
- hasReplacedButton = true;
6374
- }
6375
- if (hasReplacedButton) {
6376
- fixed = lines.join("\n");
6377
- fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
6378
- return "</Button>";
6379
- });
6380
- const openCount = (fixed.match(/<Button\b/g) || []).length;
6381
- const closeCount = (fixed.match(/<\/Button>/g) || []).length;
6382
- if (closeCount > openCount) {
6383
- let excess = closeCount - openCount;
6384
- fixed = fixed.replace(/<\/Button>/g, (m) => {
6385
- if (excess > 0) {
6386
- excess--;
6387
- return "</button>";
6157
+ for (const rule of PAGE_RELATIONSHIP_RULES) {
6158
+ if (rule.trigger.test(route)) {
6159
+ for (const rel of rule.related) {
6160
+ if (!plannedRoutes.has(rel.route)) {
6161
+ inferred.push(rel);
6162
+ plannedRoutes.add(rel.route);
6163
+ }
6388
6164
  }
6389
- return m;
6390
- });
6165
+ }
6391
6166
  }
6392
- const hasButtonImport = /import\s.*\bButton\b.*from\s+['"]@\/components\/ui\/button['"]/.test(fixed);
6393
- if (!hasButtonImport) {
6394
- const lastImportIdx = fixed.lastIndexOf("\nimport ");
6395
- if (lastImportIdx !== -1) {
6396
- const lineEnd = fixed.indexOf("\n", lastImportIdx + 1);
6397
- fixed = fixed.slice(0, lineEnd + 1) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(lineEnd + 1);
6398
- } else {
6399
- const insertAfter = hasUseClient ? fixed.indexOf("\n") + 1 : 0;
6400
- fixed = fixed.slice(0, insertAfter) + "import { Button } from '@/components/ui/button'\n" + fixed.slice(insertAfter);
6167
+ }
6168
+ return inferred;
6169
+ }
6170
+ function impliesFullWebsite(message) {
6171
+ 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(
6172
+ message
6173
+ );
6174
+ }
6175
+ function extractPageNamesFromMessage(message) {
6176
+ const pages = [];
6177
+ const known = {
6178
+ home: "/",
6179
+ landing: "/",
6180
+ dashboard: "/dashboard",
6181
+ about: "/about",
6182
+ "about us": "/about",
6183
+ contact: "/contact",
6184
+ contacts: "/contacts",
6185
+ pricing: "/pricing",
6186
+ settings: "/settings",
6187
+ account: "/account",
6188
+ "personal account": "/account",
6189
+ registration: "/registration",
6190
+ signup: "/signup",
6191
+ "sign up": "/signup",
6192
+ login: "/login",
6193
+ "sign in": "/login",
6194
+ catalogue: "/catalogue",
6195
+ catalog: "/catalog",
6196
+ blog: "/blog",
6197
+ portfolio: "/portfolio",
6198
+ features: "/features",
6199
+ services: "/services",
6200
+ faq: "/faq",
6201
+ team: "/team"
6202
+ };
6203
+ const lower = message.toLowerCase();
6204
+ for (const [key, route] of Object.entries(known)) {
6205
+ if (lower.includes(key)) {
6206
+ const name = key.split(" ").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
6207
+ const id = route.slice(1) || "home";
6208
+ if (!pages.some((p) => p.route === route)) {
6209
+ pages.push({ name, id, route });
6401
6210
  }
6402
6211
  }
6403
- fixes.push("<button> \u2192 <Button> (with import)");
6404
6212
  }
6405
- const colorMap = {
6406
- "bg-zinc-950": "bg-background",
6407
- "bg-zinc-900": "bg-background",
6408
- "bg-slate-950": "bg-background",
6409
- "bg-slate-900": "bg-background",
6410
- "bg-gray-950": "bg-background",
6411
- "bg-gray-900": "bg-background",
6412
- "bg-zinc-800": "bg-muted",
6413
- "bg-slate-800": "bg-muted",
6414
- "bg-gray-800": "bg-muted",
6415
- "bg-zinc-100": "bg-muted",
6416
- "bg-slate-100": "bg-muted",
6417
- "bg-gray-100": "bg-muted",
6418
- "bg-white": "bg-background",
6419
- "bg-black": "bg-background",
6420
- "text-white": "text-foreground",
6421
- "text-black": "text-foreground",
6422
- "text-zinc-100": "text-foreground",
6423
- "text-zinc-200": "text-foreground",
6424
- "text-slate-100": "text-foreground",
6425
- "text-gray-100": "text-foreground",
6426
- "text-zinc-400": "text-muted-foreground",
6427
- "text-zinc-500": "text-muted-foreground",
6428
- "text-slate-400": "text-muted-foreground",
6429
- "text-slate-500": "text-muted-foreground",
6430
- "text-gray-400": "text-muted-foreground",
6431
- "text-gray-500": "text-muted-foreground",
6432
- "border-zinc-700": "border-border",
6433
- "border-zinc-800": "border-border",
6434
- "border-slate-700": "border-border",
6435
- "border-gray-700": "border-border",
6436
- "border-zinc-200": "border-border",
6437
- "border-slate-200": "border-border",
6438
- "border-gray-200": "border-border"
6439
- };
6440
- const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
6441
- const isInsideTerminalBlock = (offset) => {
6442
- const preceding = fixed.slice(Math.max(0, offset - 600), offset);
6443
- if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
6444
- if (!/font-mono/.test(preceding)) return false;
6445
- const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
6446
- const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
6447
- return lastTerminal > lastClose;
6448
- };
6449
- let hadColorFix = false;
6450
- fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
6451
- if (isCodeContext(classes)) return fullMatch;
6452
- if (isInsideTerminalBlock(offset)) return fullMatch;
6453
- let result = classes;
6454
- const accentColorRe = /\b(bg|text|border)-(emerald|blue|violet|indigo|purple|teal|cyan|sky|rose|amber)-(\d+)\b/g;
6455
- result = result.replace(accentColorRe, (m, prefix, _color, shade) => {
6456
- if (colorMap[m]) {
6457
- hadColorFix = true;
6458
- return colorMap[m];
6213
+ return pages;
6214
+ }
6215
+ function normalizeRequest(request, config2) {
6216
+ const changes = request.changes;
6217
+ const VALID_TYPES = [
6218
+ "update-token",
6219
+ "add-component",
6220
+ "modify-component",
6221
+ "add-layout-block",
6222
+ "modify-layout-block",
6223
+ "add-page",
6224
+ "update-page",
6225
+ "update-navigation",
6226
+ "link-shared",
6227
+ "promote-and-link"
6228
+ ];
6229
+ if (!VALID_TYPES.includes(request.type)) {
6230
+ return { error: `Unknown action "${request.type}". Valid: ${VALID_TYPES.join(", ")}` };
6231
+ }
6232
+ const findPage = (target) => config2.pages.find(
6233
+ (p) => p.id === target || p.route === target || p.name?.toLowerCase() === String(target).toLowerCase()
6234
+ );
6235
+ switch (request.type) {
6236
+ case "update-page": {
6237
+ const page = findPage(request.target);
6238
+ if (!page && changes?.pageCode) {
6239
+ const targetStr = String(request.target);
6240
+ const id = targetStr.replace(/^\//, "") || "home";
6241
+ return {
6242
+ ...request,
6243
+ type: "add-page",
6244
+ target: "new",
6245
+ changes: {
6246
+ id,
6247
+ name: changes.name || id.charAt(0).toUpperCase() + id.slice(1) || "Home",
6248
+ route: targetStr.startsWith("/") ? targetStr : `/${targetStr}`,
6249
+ ...changes
6250
+ }
6251
+ };
6459
6252
  }
6460
- const n = parseInt(shade);
6461
- if (prefix === "bg") {
6462
- if (n >= 500 && n <= 700) {
6463
- hadColorFix = true;
6464
- return "bg-primary";
6465
- }
6466
- if (n >= 100 && n <= 200) {
6467
- hadColorFix = true;
6468
- return "bg-primary/10";
6469
- }
6470
- if (n >= 800) {
6471
- hadColorFix = true;
6472
- return "bg-muted";
6473
- }
6253
+ if (!page) {
6254
+ const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
6255
+ return { error: `Page "${request.target}" not found. Available: ${available || "none"}` };
6474
6256
  }
6475
- if (prefix === "text") {
6476
- if (n >= 400 && n <= 600) {
6477
- hadColorFix = true;
6478
- return "text-primary";
6479
- }
6480
- if (n >= 100 && n <= 300) {
6481
- hadColorFix = true;
6482
- return "text-foreground";
6483
- }
6257
+ if (page.id !== request.target) {
6258
+ return { ...request, target: page.id };
6484
6259
  }
6485
- if (prefix === "border") {
6486
- hadColorFix = true;
6487
- return "border-primary";
6260
+ break;
6261
+ }
6262
+ case "add-page": {
6263
+ if (!changes) break;
6264
+ let route = changes.route || "";
6265
+ if (route && !route.startsWith("/")) route = `/${route}`;
6266
+ if (route) changes.route = route;
6267
+ const existingByRoute = config2.pages.find((p) => p.route === route);
6268
+ if (existingByRoute && route) {
6269
+ return {
6270
+ ...request,
6271
+ type: "update-page",
6272
+ target: existingByRoute.id
6273
+ };
6488
6274
  }
6489
- return m;
6490
- });
6491
- const neutralColorRe = /\b(bg|text|border)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
6492
- result = result.replace(neutralColorRe, (m, prefix, _color, shade) => {
6493
- if (colorMap[m]) {
6494
- hadColorFix = true;
6495
- return colorMap[m];
6275
+ if (!changes.id && changes.name) {
6276
+ changes.id = String(changes.name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
6496
6277
  }
6497
- const n = parseInt(shade);
6498
- if (prefix === "bg") {
6499
- if (n >= 800) {
6500
- hadColorFix = true;
6501
- return "bg-background";
6502
- }
6503
- if (n >= 100 && n <= 300) {
6504
- hadColorFix = true;
6505
- return "bg-muted";
6506
- }
6278
+ if (!changes.id && route) {
6279
+ changes.id = route.replace(/^\//, "") || "home";
6507
6280
  }
6508
- if (prefix === "text") {
6509
- if (n >= 100 && n <= 300) {
6510
- hadColorFix = true;
6511
- return "text-foreground";
6281
+ break;
6282
+ }
6283
+ case "modify-component": {
6284
+ const componentId = request.target;
6285
+ const existingComp = config2.components.find((c) => c.id === componentId);
6286
+ if (!existingComp) {
6287
+ return {
6288
+ ...request,
6289
+ type: "add-component",
6290
+ target: "new"
6291
+ };
6292
+ }
6293
+ if (changes) {
6294
+ if (typeof changes.id === "string" && changes.id !== componentId) {
6295
+ const targetExists = config2.components.some((c) => c.id === changes.id);
6296
+ if (!targetExists) {
6297
+ return { ...request, type: "add-component", target: "new" };
6298
+ }
6299
+ return {
6300
+ error: `Cannot change component "${componentId}" to "${changes.id}" \u2014 "${changes.id}" already exists.`
6301
+ };
6512
6302
  }
6513
- if (n >= 400 && n <= 600) {
6514
- hadColorFix = true;
6515
- return "text-muted-foreground";
6303
+ if (typeof changes.name === "string") {
6304
+ const newName = changes.name.toLowerCase();
6305
+ const curName = existingComp.name.toLowerCase();
6306
+ const curId = componentId.toLowerCase();
6307
+ const nameOk = newName === curName || newName === curId || newName.includes(curId) || curId.includes(newName);
6308
+ if (!nameOk) {
6309
+ delete changes.name;
6310
+ }
6516
6311
  }
6517
6312
  }
6518
- if (prefix === "border") {
6519
- hadColorFix = true;
6520
- return "border-border";
6521
- }
6522
- return m;
6523
- });
6524
- if (result !== classes) return `className="${result}"`;
6525
- return fullMatch;
6526
- });
6527
- if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
6528
- const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
6529
- if (lucideImportMatch) {
6530
- let lucideExports = null;
6531
- try {
6532
- const { createRequire } = await import("module");
6533
- const require2 = createRequire(process.cwd() + "/package.json");
6534
- const lr = require2("lucide-react");
6535
- lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
6536
- } catch {
6313
+ break;
6537
6314
  }
6538
- if (lucideExports) {
6539
- const nonLucideImports = /* @__PURE__ */ new Set();
6540
- for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from\s*["'](?!lucide-react)([^"']+)["']/g)) {
6541
- m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => nonLucideImports.add(n));
6542
- }
6543
- const iconNames = lucideImportMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
6544
- const duplicates = iconNames.filter((name) => nonLucideImports.has(name));
6545
- let newImport = lucideImportMatch[1];
6546
- for (const dup of duplicates) {
6547
- newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
6548
- fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
6549
- }
6550
- const invalid = iconNames.filter((name) => !lucideExports.has(name) && !nonLucideImports.has(name));
6551
- if (invalid.length > 0) {
6552
- const fallback = "Circle";
6553
- for (const bad of invalid) {
6554
- const re = new RegExp(`\\b${bad}\\b`, "g");
6555
- newImport = newImport.replace(re, fallback);
6556
- fixed = fixed.replace(re, fallback);
6315
+ case "add-component": {
6316
+ if (changes) {
6317
+ const shadcn = changes.shadcnComponent;
6318
+ const id = changes.id;
6319
+ if (shadcn && id && id !== shadcn) {
6320
+ changes.id = shadcn;
6557
6321
  }
6558
- fixes.push(`invalid lucide icons \u2192 ${fallback}: ${invalid.join(", ")}`);
6559
6322
  }
6560
- if (duplicates.length > 0 || invalid.length > 0) {
6561
- const importedNames = [
6562
- ...new Set(
6563
- newImport.split(",").map((s) => s.trim()).filter(Boolean)
6564
- )
6565
- ];
6566
- const originalImportLine = lucideImportMatch[0];
6567
- fixed = fixed.replace(originalImportLine, `import { ${importedNames.join(", ")} } from "lucide-react"`);
6323
+ break;
6324
+ }
6325
+ case "link-shared": {
6326
+ if (changes) {
6327
+ const page = findPage(request.target);
6328
+ if (!page) {
6329
+ const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
6330
+ return { error: `Page "${request.target}" not found for link-shared. Available: ${available || "none"}` };
6331
+ }
6332
+ if (page.id !== request.target) {
6333
+ return { ...request, target: page.id };
6334
+ }
6568
6335
  }
6336
+ break;
6569
6337
  }
6570
- }
6571
- const lucideImportMatch2 = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
6572
- if (lucideImportMatch2) {
6573
- let lucideExports2 = null;
6574
- try {
6575
- const { createRequire } = await import("module");
6576
- const req = createRequire(process.cwd() + "/package.json");
6577
- const lr = req("lucide-react");
6578
- lucideExports2 = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
6579
- } catch {
6338
+ case "promote-and-link": {
6339
+ const sourcePage = findPage(request.target);
6340
+ if (!sourcePage) {
6341
+ const available = config2.pages.map((p) => `${p.name} (${p.route})`).join(", ");
6342
+ return {
6343
+ error: `Source page "${request.target}" not found for promote-and-link. Available: ${available || "none"}`
6344
+ };
6345
+ }
6346
+ if (sourcePage.id !== request.target) {
6347
+ return { ...request, target: sourcePage.id };
6348
+ }
6349
+ break;
6580
6350
  }
6581
- if (lucideExports2) {
6582
- const allImportedNames = /* @__PURE__ */ new Set();
6583
- for (const m of fixed.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
6584
- m[1].split(",").map((s) => s.trim()).filter(Boolean).forEach((n) => allImportedNames.add(n));
6351
+ }
6352
+ return request;
6353
+ }
6354
+ function applyDefaults(request) {
6355
+ if (request.type === "add-page" && request.changes && typeof request.changes === "object") {
6356
+ const changes = request.changes;
6357
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6358
+ const name = changes.name || "New Page";
6359
+ let id = changes.id || name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
6360
+ if (!/^[a-z]/.test(id)) id = `page-${id}`;
6361
+ const route = changes.route || `/${id}`;
6362
+ const hasPageCode = typeof changes.pageCode === "string" && changes.pageCode.trim() !== "";
6363
+ const base = {
6364
+ id,
6365
+ name,
6366
+ route: route.startsWith("/") ? route : `/${route}`,
6367
+ layout: changes.layout || "centered",
6368
+ title: changes.title || name,
6369
+ description: changes.description || `${name} page`,
6370
+ createdAt: changes.createdAt || now,
6371
+ updatedAt: changes.updatedAt || now,
6372
+ requiresAuth: changes.requiresAuth ?? false,
6373
+ noIndex: changes.noIndex ?? false
6374
+ };
6375
+ const sections = Array.isArray(changes.sections) ? changes.sections.map((section, idx) => ({
6376
+ id: section.id || `section-${idx}`,
6377
+ name: section.name || `Section ${idx + 1}`,
6378
+ componentId: section.componentId || "button",
6379
+ order: typeof section.order === "number" ? section.order : idx,
6380
+ props: section.props || {}
6381
+ })) : [];
6382
+ return {
6383
+ ...request,
6384
+ changes: {
6385
+ ...base,
6386
+ sections,
6387
+ ...hasPageCode ? { pageCode: changes.pageCode, generatedWithPageCode: true } : {},
6388
+ ...changes.pageType ? { pageType: changes.pageType } : {},
6389
+ ...changes.structuredContent ? { structuredContent: changes.structuredContent } : {}
6585
6390
  }
6586
- for (const m of fixed.matchAll(/import\s+([A-Z]\w+)\s+from/g)) {
6587
- allImportedNames.add(m[1]);
6391
+ };
6392
+ }
6393
+ if (request.type === "add-component" && request.changes && typeof request.changes === "object") {
6394
+ const changes = request.changes;
6395
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6396
+ const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
6397
+ let normalizedVariants = [];
6398
+ if (Array.isArray(changes.variants)) {
6399
+ normalizedVariants = changes.variants.map((v) => {
6400
+ if (typeof v === "string") return { name: v, className: "" };
6401
+ if (v && typeof v === "object" && "name" in v) {
6402
+ return {
6403
+ name: v.name,
6404
+ className: v.className ?? ""
6405
+ };
6406
+ }
6407
+ return { name: "default", className: "" };
6408
+ });
6409
+ }
6410
+ let normalizedSizes = [];
6411
+ if (Array.isArray(changes.sizes)) {
6412
+ normalizedSizes = changes.sizes.map((s) => {
6413
+ if (typeof s === "string") {
6414
+ const name = validSizeNames.includes(s) ? s : "md";
6415
+ return { name, className: "" };
6416
+ }
6417
+ if (s && typeof s === "object" && "name" in s) {
6418
+ const raw = s.name;
6419
+ const name = validSizeNames.includes(raw) ? raw : "md";
6420
+ return { name, className: s.className ?? "" };
6421
+ }
6422
+ return { name: "md", className: "" };
6423
+ });
6424
+ }
6425
+ return {
6426
+ ...request,
6427
+ changes: {
6428
+ ...changes,
6429
+ variants: normalizedVariants,
6430
+ sizes: normalizedSizes,
6431
+ createdAt: now,
6432
+ updatedAt: now
6588
6433
  }
6589
- const lucideImported = new Set(
6590
- lucideImportMatch2[1].split(",").map((s) => s.trim()).filter(Boolean)
6591
- );
6592
- const jsxIconRefs = [...new Set([...fixed.matchAll(/<([A-Z][a-zA-Z]*Icon)\s/g)].map((m) => m[1]))];
6593
- const missing = [];
6594
- for (const ref of jsxIconRefs) {
6595
- if (allImportedNames.has(ref)) continue;
6596
- if (fixed.includes(`function ${ref}`) || fixed.includes(`const ${ref}`)) continue;
6597
- const baseName = ref.replace(/Icon$/, "");
6598
- if (lucideExports2.has(ref)) {
6599
- missing.push(ref);
6600
- lucideImported.add(ref);
6601
- } else if (lucideExports2.has(baseName)) {
6602
- const re = new RegExp(`\\b${ref}\\b`, "g");
6603
- fixed = fixed.replace(re, baseName);
6604
- missing.push(baseName);
6605
- lucideImported.add(baseName);
6606
- fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
6607
- } else {
6608
- const fallback = "Circle";
6609
- const re = new RegExp(`\\b${ref}\\b`, "g");
6610
- fixed = fixed.replace(re, fallback);
6611
- lucideImported.add(fallback);
6612
- fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
6434
+ };
6435
+ }
6436
+ if (request.type === "modify-component" && request.changes && typeof request.changes === "object") {
6437
+ const changes = request.changes;
6438
+ const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
6439
+ let normalizedVariants;
6440
+ if (Array.isArray(changes.variants)) {
6441
+ normalizedVariants = changes.variants.map((v) => {
6442
+ if (typeof v === "string") return { name: v, className: "" };
6443
+ if (v && typeof v === "object" && "name" in v) {
6444
+ return {
6445
+ name: v.name,
6446
+ className: v.className ?? ""
6447
+ };
6448
+ }
6449
+ return { name: "default", className: "" };
6450
+ });
6451
+ }
6452
+ let normalizedSizes;
6453
+ if (Array.isArray(changes.sizes)) {
6454
+ normalizedSizes = changes.sizes.map((s) => {
6455
+ if (typeof s === "string") {
6456
+ const name = validSizeNames.includes(s) ? s : "md";
6457
+ return { name, className: "" };
6458
+ }
6459
+ if (s && typeof s === "object" && "name" in s) {
6460
+ const raw = s.name;
6461
+ const name = validSizeNames.includes(raw) ? raw : "md";
6462
+ return { name, className: s.className ?? "" };
6613
6463
  }
6464
+ return { name: "md", className: "" };
6465
+ });
6466
+ }
6467
+ return {
6468
+ ...request,
6469
+ changes: {
6470
+ ...changes,
6471
+ ...normalizedVariants !== void 0 && { variants: normalizedVariants },
6472
+ ...normalizedSizes !== void 0 && { sizes: normalizedSizes }
6614
6473
  }
6615
- if (missing.length > 0) {
6616
- const allNames = [...lucideImported];
6617
- const origLine = lucideImportMatch2[0];
6618
- fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
6619
- fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
6474
+ };
6475
+ }
6476
+ return request;
6477
+ }
6478
+
6479
+ // src/utils/page-analyzer.ts
6480
+ var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
6481
+ 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;
6482
+ function analyzePageCode(code) {
6483
+ return {
6484
+ sections: extractSections(code),
6485
+ componentUsage: extractComponentUsage(code),
6486
+ iconCount: extractIconCount(code),
6487
+ layoutPattern: inferLayoutPattern(code),
6488
+ hasForm: detectFormUsage(code),
6489
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
6490
+ };
6491
+ }
6492
+ function extractSections(code) {
6493
+ const sections = [];
6494
+ const seen = /* @__PURE__ */ new Set();
6495
+ const commentRe = /\{\/\*\s*(.+?)\s*\*\/\}/g;
6496
+ let m;
6497
+ while ((m = commentRe.exec(code)) !== null) {
6498
+ const raw = m[1].trim();
6499
+ const name = raw.replace(/[─━—–]+/g, "").replace(/\s*section\s*$/i, "").replace(/^section\s*:\s*/i, "").trim();
6500
+ if (!name || name.length <= 1 || name.length >= 40) continue;
6501
+ if (seen.has(name.toLowerCase())) continue;
6502
+ const wordCount = name.split(/\s+/).length;
6503
+ if (wordCount > 5) continue;
6504
+ if (/[{}()=<>;:`"']/.test(name)) continue;
6505
+ if (/^[a-z]/.test(name) && wordCount > 2) continue;
6506
+ if (VISUAL_WORDS.test(name)) continue;
6507
+ seen.add(name.toLowerCase());
6508
+ sections.push({ name, order: sections.length });
6509
+ }
6510
+ if (sections.length === 0) {
6511
+ const sectionTagRe = /<section[^>]*>[\s\S]*?<h[12][^>]*>\s*([^<]+)/g;
6512
+ while ((m = sectionTagRe.exec(code)) !== null) {
6513
+ const name = m[1].trim();
6514
+ if (name && name.length > 1 && name.length < 40 && !seen.has(name.toLowerCase())) {
6515
+ seen.add(name.toLowerCase());
6516
+ sections.push({ name, order: sections.length });
6620
6517
  }
6621
6518
  }
6622
6519
  }
6623
- fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
6624
- const cleaned = inner.replace(/\s{2,}/g, " ").trim();
6625
- return `className="${cleaned}"`;
6626
- });
6627
- let imgCounter = 1;
6628
- const beforeImgFix = fixed;
6629
- fixed = fixed.replace(/["']\/api\/placeholder\/(\d+)\/(\d+)["']/g, (_m, w, h) => {
6630
- return `"https://picsum.photos/${w}/${h}?random=${imgCounter++}"`;
6631
- });
6632
- fixed = fixed.replace(/["']\/placeholder-avatar[^"']*["']/g, () => {
6633
- return `"https://i.pravatar.cc/150?u=user${imgCounter++}"`;
6634
- });
6635
- fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
6636
- const height = h || w;
6637
- return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
6638
- });
6639
- fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
6640
- return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
6641
- });
6642
- fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
6643
- return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
6644
- });
6645
- if (fixed !== beforeImgFix) {
6646
- fixes.push("placeholder images \u2192 working URLs (picsum/pravatar)");
6520
+ return sections;
6521
+ }
6522
+ function extractComponentUsage(code) {
6523
+ const usage = {};
6524
+ const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
6525
+ const importedComponents = [];
6526
+ let m;
6527
+ while ((m = importRe.exec(code)) !== null) {
6528
+ const names = m[1].split(",").map((s) => s.trim()).filter(Boolean);
6529
+ importedComponents.push(...names);
6530
+ }
6531
+ for (const comp of importedComponents) {
6532
+ const re = new RegExp(`<${comp}[\\s/>]`, "g");
6533
+ const matches = code.match(re);
6534
+ usage[comp] = matches ? matches.length : 0;
6535
+ }
6536
+ return usage;
6537
+ }
6538
+ function extractIconCount(code) {
6539
+ const m = code.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
6540
+ if (!m) return 0;
6541
+ return m[1].split(",").map((s) => s.trim()).filter(Boolean).length;
6542
+ }
6543
+ function inferLayoutPattern(code) {
6544
+ const funcBodyMatch = code.match(/return\s*\(\s*(<[^]*)/s);
6545
+ const topLevel = funcBodyMatch ? funcBodyMatch[1].slice(0, 500) : code.slice(0, 800);
6546
+ if (/grid-cols|grid\s+md:grid-cols|grid\s+lg:grid-cols/.test(topLevel)) return "grid";
6547
+ if (/sidebar|aside/.test(topLevel)) return "sidebar";
6548
+ if (/max-w-\d|mx-auto|container/.test(topLevel)) return "centered";
6549
+ if (/min-h-screen|min-h-svh/.test(topLevel)) return "full-width";
6550
+ return "unknown";
6551
+ }
6552
+ function detectFormUsage(code) {
6553
+ const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
6554
+ let m;
6555
+ while ((m = importRe.exec(code)) !== null) {
6556
+ const names = m[1].split(",").map((s) => s.trim());
6557
+ if (names.some((n) => FORM_COMPONENTS.has(n))) return true;
6558
+ }
6559
+ return false;
6560
+ }
6561
+ function summarizePageAnalysis(pageName, route, analysis) {
6562
+ const parts = [`${pageName} (${route})`];
6563
+ if (analysis.sections && analysis.sections.length > 0) {
6564
+ parts.push(`sections: ${analysis.sections.map((s) => s.name).join(", ")}`);
6565
+ }
6566
+ if (analysis.componentUsage) {
6567
+ const entries = Object.entries(analysis.componentUsage).filter(([, c]) => c > 0);
6568
+ if (entries.length > 0) {
6569
+ parts.push(`uses: ${entries.map(([n, c]) => `${n}(${c})`).join(", ")}`);
6570
+ }
6571
+ }
6572
+ if (analysis.layoutPattern && analysis.layoutPattern !== "unknown") {
6573
+ parts.push(`layout: ${analysis.layoutPattern}`);
6574
+ }
6575
+ if (analysis.hasForm) parts.push("has-form");
6576
+ return `- ${parts.join(". ")}`;
6577
+ }
6578
+
6579
+ // src/utils/concurrency.ts
6580
+ async function pMap(items, fn, concurrency = 3) {
6581
+ const results = new Array(items.length);
6582
+ let nextIndex = 0;
6583
+ async function worker() {
6584
+ while (nextIndex < items.length) {
6585
+ const i = nextIndex++;
6586
+ results[i] = await fn(items[i], i);
6587
+ }
6588
+ }
6589
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
6590
+ await Promise.all(workers);
6591
+ return results;
6592
+ }
6593
+
6594
+ // src/commands/chat/split-generator.ts
6595
+ function buildExistingPagesContext(config2) {
6596
+ const pages = config2.pages || [];
6597
+ const analyzed = pages.filter((p) => p.pageAnalysis);
6598
+ if (analyzed.length === 0) return "";
6599
+ const lines = analyzed.map((p) => {
6600
+ return summarizePageAnalysis(p.name || p.id, p.route, p.pageAnalysis);
6601
+ });
6602
+ let ctx = `EXISTING PAGES CONTEXT:
6603
+ ${lines.join("\n")}
6604
+
6605
+ Use consistent component choices, spacing, and layout patterns across all pages. Match the style and structure of existing pages.`;
6606
+ const sp = config2.stylePatterns;
6607
+ if (sp && typeof sp === "object") {
6608
+ const parts = [];
6609
+ if (sp.card) parts.push(`Cards: ${sp.card}`);
6610
+ if (sp.section) parts.push(`Sections: ${sp.section}`);
6611
+ if (sp.terminal) parts.push(`Terminal blocks: ${sp.terminal}`);
6612
+ if (sp.iconContainer) parts.push(`Icon containers: ${sp.iconContainer}`);
6613
+ if (sp.heroHeadline) parts.push(`Hero headline: ${sp.heroHeadline}`);
6614
+ if (sp.sectionTitle) parts.push(`Section title: ${sp.sectionTitle}`);
6615
+ if (parts.length > 0) {
6616
+ ctx += `
6617
+
6618
+ PROJECT STYLE PATTERNS (from sync \u2014 match these exactly):
6619
+ ${parts.join("\n")}`;
6620
+ }
6621
+ }
6622
+ return ctx;
6623
+ }
6624
+ function extractStyleContext(pageCode) {
6625
+ const unique = (arr) => [...new Set(arr)];
6626
+ 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"));
6627
+ const sectionSpacing = unique(pageCode.match(/py-\d+(?:\s+md:py-\d+)?/g) || []);
6628
+ const headingStyles = unique(pageCode.match(/text-(?:\d*xl|lg)\s+font-(?:bold|semibold|medium)/g) || []);
6629
+ const colorPatterns = unique(
6630
+ (pageCode.match(
6631
+ /(?:text|bg|border)-(?:primary|secondary|muted|accent|card|destructive|foreground|background)\S*/g
6632
+ ) || []).concat(
6633
+ pageCode.match(
6634
+ /(?:text|bg|border)-(?:emerald|blue|violet|rose|amber|zinc|slate|gray|green|red|orange|indigo|purple|teal|cyan)\S*/g
6635
+ ) || []
6636
+ )
6637
+ );
6638
+ const iconPatterns = unique(pageCode.match(/(?:rounded-\S+\s+)?p-\d+(?:\.\d+)?\s*(?:bg-\S+)?/g) || []).filter(
6639
+ (p) => p.includes("bg-") || p.includes("rounded")
6640
+ );
6641
+ const buttonPatterns = unique(
6642
+ (pageCode.match(/className="[^"]*(?:hover:|active:)[^"]*"/g) || []).map((m) => m.replace(/className="|"/g, "")).filter((c) => c.includes("px-") || c.includes("py-") || c.includes("rounded"))
6643
+ );
6644
+ const bgPatterns = unique(pageCode.match(/bg-(?:muted|card|background|zinc|slate|gray)\S*/g) || []);
6645
+ const gapPatterns = unique(pageCode.match(/gap-\d+/g) || []);
6646
+ const gridPatterns = unique(pageCode.match(/grid-cols-\d+|md:grid-cols-\d+|lg:grid-cols-\d+/g) || []);
6647
+ const containerPatterns = unique(pageCode.match(/container\s+max-w-\S+|max-w-\d+xl\s+mx-auto/g) || []);
6648
+ const lines = [];
6649
+ if (containerPatterns.length > 0) {
6650
+ lines.push(`Container (MUST match for alignment with header/footer): ${containerPatterns[0]} px-4`);
6651
+ }
6652
+ if (cardClasses.length > 0) lines.push(`Cards: ${unique(cardClasses).slice(0, 4).join(" | ")}`);
6653
+ if (sectionSpacing.length > 0) lines.push(`Section spacing: ${sectionSpacing.join(", ")}`);
6654
+ if (headingStyles.length > 0) lines.push(`Headings: ${headingStyles.join(", ")}`);
6655
+ if (colorPatterns.length > 0) lines.push(`Colors: ${colorPatterns.slice(0, 15).join(", ")}`);
6656
+ if (iconPatterns.length > 0) lines.push(`Icon containers: ${iconPatterns.slice(0, 4).join(" | ")}`);
6657
+ if (buttonPatterns.length > 0) lines.push(`Buttons: ${buttonPatterns.slice(0, 3).join(" | ")}`);
6658
+ if (bgPatterns.length > 0) lines.push(`Section backgrounds: ${bgPatterns.slice(0, 6).join(", ")}`);
6659
+ if (gapPatterns.length > 0) lines.push(`Gaps: ${gapPatterns.join(", ")}`);
6660
+ if (gridPatterns.length > 0) lines.push(`Grids: ${gridPatterns.join(", ")}`);
6661
+ if (lines.length === 0) return "";
6662
+ return `STYLE CONTEXT (match these patterns exactly for visual consistency with the Home page):
6663
+ ${lines.map((l) => ` - ${l}`).join("\n")}`;
6664
+ }
6665
+ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
6666
+ let pageNames = [];
6667
+ spinner.start("Phase 1/4 \u2014 Planning pages...");
6668
+ try {
6669
+ const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
6670
+ const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
6671
+ pageNames = pageReqs.map((r) => {
6672
+ const c = r.changes;
6673
+ const name = c.name || c.id || "page";
6674
+ const id = c.id || name.toLowerCase().replace(/\s+/g, "-");
6675
+ const route = c.route || `/${id}`;
6676
+ return { name, id, route };
6677
+ });
6678
+ } catch {
6679
+ spinner.text = "AI plan failed \u2014 extracting pages from your request...";
6680
+ }
6681
+ if (pageNames.length === 0) {
6682
+ pageNames = extractPageNamesFromMessage(message);
6683
+ }
6684
+ if (pageNames.length === 0) {
6685
+ spinner.fail("Could not determine pages to create");
6686
+ return [];
6687
+ }
6688
+ pageNames = deduplicatePages(pageNames);
6689
+ const hasHomePage = pageNames.some((p) => p.route === "/");
6690
+ if (!hasHomePage) {
6691
+ const userPages = (modCtx.config.pages || []).filter(
6692
+ (p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
6693
+ );
6694
+ const isFreshProject = userPages.length === 0;
6695
+ if (isFreshProject || impliesFullWebsite(message)) {
6696
+ pageNames.unshift({ name: "Home", id: "home", route: "/" });
6697
+ }
6698
+ }
6699
+ const existingRoutes = new Set((modCtx.config.pages || []).map((p) => p.route).filter(Boolean));
6700
+ const inferred = inferRelatedPages(pageNames).filter((p) => !existingRoutes.has(p.route));
6701
+ if (inferred.length > 0) {
6702
+ pageNames.push(...inferred);
6703
+ pageNames = deduplicatePages(pageNames);
6647
6704
  }
6648
- return { code: fixed, fixes };
6649
- }
6650
- function formatIssues(issues) {
6651
- if (issues.length === 0) return "";
6652
- const errors = issues.filter((i) => i.severity === "error");
6653
- const warnings = issues.filter((i) => i.severity === "warning");
6654
- const infos = issues.filter((i) => i.severity === "info");
6655
- const lines = [];
6656
- if (errors.length > 0) {
6657
- lines.push(` \u274C ${errors.length} error(s):`);
6658
- for (const e of errors) {
6659
- lines.push(` L${e.line}: [${e.type}] ${e.message}`);
6705
+ const allRoutes = pageNames.map((p) => p.route).join(", ");
6706
+ const allPagesList = pageNames.map((p) => `${p.name} (${p.route})`).join(", ");
6707
+ const inferredNote = inferred.length > 0 ? ` (${inferred.length} auto-inferred)` : "";
6708
+ spinner.succeed(`Phase 1/4 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
6709
+ const homeIdx = pageNames.findIndex((p) => p.route === "/");
6710
+ const homePage = homeIdx !== -1 ? pageNames[homeIdx] : pageNames[0];
6711
+ const remainingPages = pageNames.filter((_, i) => i !== (homeIdx !== -1 ? homeIdx : 0));
6712
+ spinner.start(`Phase 2/4 \u2014 Generating ${homePage.name} page (sets design direction)...`);
6713
+ let homeRequest = null;
6714
+ let homePageCode = "";
6715
+ try {
6716
+ const homeResult = await parseModification(
6717
+ `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.`,
6718
+ modCtx,
6719
+ provider,
6720
+ parseOpts
6721
+ );
6722
+ const codePage = homeResult.requests.find((r) => r.type === "add-page");
6723
+ if (codePage) {
6724
+ homeRequest = codePage;
6725
+ homePageCode = codePage.changes?.pageCode || "";
6660
6726
  }
6727
+ } catch {
6661
6728
  }
6662
- if (warnings.length > 0) {
6663
- lines.push(` \u26A0\uFE0F ${warnings.length} warning(s):`);
6664
- for (const w of warnings) {
6665
- lines.push(` L${w.line}: [${w.type}] ${w.message}`);
6666
- }
6729
+ if (!homeRequest) {
6730
+ homeRequest = {
6731
+ type: "add-page",
6732
+ target: "new",
6733
+ changes: { id: homePage.id, name: homePage.name, route: homePage.route }
6734
+ };
6667
6735
  }
6668
- if (infos.length > 0) {
6669
- lines.push(` \u2139\uFE0F ${infos.length} info:`);
6670
- for (const i of infos) {
6671
- lines.push(` L${i.line}: [${i.type}] ${i.message}`);
6736
+ spinner.succeed(`Phase 2/4 \u2014 ${homePage.name} page generated`);
6737
+ spinner.start("Phase 3/4 \u2014 Extracting design patterns...");
6738
+ const styleContext = homePageCode ? extractStyleContext(homePageCode) : "";
6739
+ if (styleContext) {
6740
+ const lineCount = styleContext.split("\n").length - 1;
6741
+ spinner.succeed(`Phase 3/4 \u2014 Extracted ${lineCount} style patterns from ${homePage.name}`);
6742
+ } else {
6743
+ spinner.succeed("Phase 3/4 \u2014 No style patterns extracted (Home page had no code)");
6744
+ }
6745
+ if (remainingPages.length === 0) {
6746
+ return [homeRequest];
6747
+ }
6748
+ spinner.start(`Phase 4/4 \u2014 Generating ${remainingPages.length} pages in parallel...`);
6749
+ 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.";
6750
+ 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="#".`;
6751
+ 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.';
6752
+ const existingPagesContext = buildExistingPagesContext(modCtx.config);
6753
+ const AI_CONCURRENCY = 3;
6754
+ let phase4Done = 0;
6755
+ const remainingRequests = await pMap(
6756
+ remainingPages,
6757
+ async ({ name, id, route }) => {
6758
+ const prompt = [
6759
+ `Create ONE page called "${name}" at route "${route}".`,
6760
+ `Context: ${message}.`,
6761
+ `Generate complete pageCode for this single page only. Do not generate other pages.`,
6762
+ sharedNote,
6763
+ routeNote,
6764
+ alignmentNote,
6765
+ existingPagesContext,
6766
+ styleContext
6767
+ ].filter(Boolean).join("\n\n");
6768
+ try {
6769
+ const result = await parseModification(prompt, modCtx, provider, parseOpts);
6770
+ phase4Done++;
6771
+ spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
6772
+ const codePage = result.requests.find((r) => r.type === "add-page");
6773
+ return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
6774
+ } catch {
6775
+ phase4Done++;
6776
+ spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
6777
+ return { type: "add-page", target: "new", changes: { id, name, route } };
6778
+ }
6779
+ },
6780
+ AI_CONCURRENCY
6781
+ );
6782
+ const allRequests = [homeRequest, ...remainingRequests];
6783
+ const emptyPages = allRequests.filter((r) => r.type === "add-page" && !r.changes?.pageCode);
6784
+ if (emptyPages.length > 0 && emptyPages.length <= 5) {
6785
+ spinner.text = `Retrying ${emptyPages.length} page(s) without code...`;
6786
+ for (const req of emptyPages) {
6787
+ const page = req.changes;
6788
+ const pageName = page.name || page.id || "page";
6789
+ const pageRoute = page.route || `/${pageName.toLowerCase()}`;
6790
+ try {
6791
+ const retryResult = await parseModification(
6792
+ `Create ONE page called "${pageName}" at route "${pageRoute}". Context: ${message}. Generate complete pageCode for this single page only.`,
6793
+ modCtx,
6794
+ provider,
6795
+ parseOpts
6796
+ );
6797
+ const codePage = retryResult.requests.find((r) => r.type === "add-page");
6798
+ if (codePage && codePage.changes?.pageCode) {
6799
+ const idx = allRequests.indexOf(req);
6800
+ if (idx !== -1) allRequests[idx] = codePage;
6801
+ }
6802
+ } catch {
6803
+ }
6672
6804
  }
6673
6805
  }
6674
- return lines.join("\n");
6806
+ const withCode = allRequests.filter((r) => r.changes?.pageCode).length;
6807
+ spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
6808
+ return allRequests;
6675
6809
  }
6676
6810
 
6811
+ // src/commands/chat/modification-handler.ts
6812
+ import { resolve as resolve7 } from "path";
6813
+ import { mkdir as mkdir4 } from "fs/promises";
6814
+ import { dirname as dirname6 } from "path";
6815
+ import chalk11 from "chalk";
6816
+ import {
6817
+ getTemplateForPageType,
6818
+ loadManifest as loadManifest5,
6819
+ saveManifest,
6820
+ updateUsedIn,
6821
+ findSharedComponentByIdOrName,
6822
+ generateSharedComponent as generateSharedComponent3
6823
+ } from "@getcoherent/core";
6824
+
6677
6825
  // src/commands/chat/code-generator.ts
6678
6826
  import { resolve as resolve6 } from "path";
6679
6827
  import { existsSync as existsSync14 } from "fs";
6680
- import { mkdir as mkdir2, readFile as readFile5 } from "fs/promises";
6828
+ import { mkdir as mkdir3 } from "fs/promises";
6681
6829
  import { dirname as dirname5 } from "path";
6682
6830
  import {
6683
6831
  ComponentGenerator as ComponentGenerator2,
@@ -6686,6 +6834,39 @@ import {
6686
6834
  } from "@getcoherent/core";
6687
6835
  import { integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout2, generateSharedComponent as generateSharedComponent2 } from "@getcoherent/core";
6688
6836
  import chalk9 from "chalk";
6837
+
6838
+ // src/utils/file-hashes.ts
6839
+ import { createHash } from "crypto";
6840
+ import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
6841
+ import { join as join9 } from "path";
6842
+ var HASHES_FILE = ".coherent/file-hashes.json";
6843
+ async function computeFileHash(filePath) {
6844
+ const content = await readFile5(filePath, "utf-8");
6845
+ return createHash("sha256").update(content).digest("hex");
6846
+ }
6847
+ async function loadHashes(projectRoot) {
6848
+ try {
6849
+ const raw = await readFile5(join9(projectRoot, HASHES_FILE), "utf-8");
6850
+ return JSON.parse(raw);
6851
+ } catch {
6852
+ return {};
6853
+ }
6854
+ }
6855
+ async function saveHashes(projectRoot, hashes) {
6856
+ const dir = join9(projectRoot, ".coherent");
6857
+ await mkdir2(dir, { recursive: true });
6858
+ await writeFile4(join9(projectRoot, HASHES_FILE), JSON.stringify(hashes, null, 2) + "\n");
6859
+ }
6860
+ async function isManuallyEdited(filePath, storedHash) {
6861
+ try {
6862
+ const currentHash = await computeFileHash(filePath);
6863
+ return currentHash !== storedHash;
6864
+ } catch {
6865
+ return false;
6866
+ }
6867
+ }
6868
+
6869
+ // src/commands/chat/code-generator.ts
6689
6870
  async function validateAndFixGeneratedCode(projectRoot, code, options = {}) {
6690
6871
  const fixes = [];
6691
6872
  let fixed = fixEscapedClosingQuotes(code);
@@ -6748,58 +6929,71 @@ async function regeneratePage(pageId, config2, projectRoot) {
6748
6929
  const route = page.route || "/";
6749
6930
  const isAuth = isAuthRoute(route) || isAuthRoute(page.name || page.id || "");
6750
6931
  const filePath = routeToFsPath(projectRoot, route, isAuth);
6751
- await mkdir2(dirname5(filePath), { recursive: true });
6932
+ await mkdir3(dirname5(filePath), { recursive: true });
6752
6933
  await writeFile(filePath, code);
6753
6934
  }
6754
- async function regenerateLayout(config2, projectRoot) {
6755
- const layout = config2.pages[0]?.layout || "centered";
6935
+ async function canOverwriteShared(projectRoot, componentFile, storedHashes) {
6936
+ const filePath = resolve6(projectRoot, componentFile);
6937
+ if (!existsSync14(filePath)) return true;
6938
+ const storedHash = storedHashes[componentFile];
6939
+ if (!storedHash) return true;
6940
+ const edited = await isManuallyEdited(filePath, storedHash);
6941
+ if (edited) {
6942
+ console.log(chalk9.yellow(` \u26A0 Skipping ${componentFile} \u2014 manually edited since last generation`));
6943
+ }
6944
+ return !edited;
6945
+ }
6946
+ async function regenerateLayout(config2, projectRoot, options = { navChanged: false }) {
6756
6947
  const appType = config2.settings.appType || "multi-page";
6757
6948
  const generator = new PageGenerator(config2);
6758
- const code = await generator.generateLayout(layout, appType, { skipNav: true });
6759
- const layoutPath = resolve6(projectRoot, "app", "layout.tsx");
6760
- await writeFile(layoutPath, code);
6949
+ const initialized = config2.settings.initialized !== false;
6950
+ const hashes = options.storedHashes ?? {};
6951
+ if (!initialized) {
6952
+ const layout = config2.pages[0]?.layout || "centered";
6953
+ const code = await generator.generateLayout(layout, appType, { skipNav: true });
6954
+ await writeFile(resolve6(projectRoot, "app", "layout.tsx"), code);
6955
+ }
6761
6956
  if (config2.navigation?.enabled && appType === "multi-page") {
6762
6957
  const navType = config2.navigation.type || "header";
6763
- if (navType === "header" || navType === "both") {
6764
- const headerCode = generator.generateSharedHeaderCode();
6765
- await generateSharedComponent2(projectRoot, {
6766
- name: "Header",
6767
- type: "layout",
6768
- code: headerCode,
6769
- description: "Main site header with navigation and theme toggle",
6770
- usedIn: ["app/layout.tsx"],
6771
- overwrite: true
6772
- });
6773
- }
6774
- let shouldOverwriteFooter = false;
6775
- try {
6776
- const footerPath = resolve6(projectRoot, "components", "shared", "footer.tsx");
6777
- const existing = await readFile5(footerPath, "utf-8");
6778
- shouldOverwriteFooter = existing.includes("Coherent Design Method");
6779
- } catch {
6780
- shouldOverwriteFooter = true;
6781
- }
6782
- if (shouldOverwriteFooter) {
6783
- const footerCode = generator.generateSharedFooterCode();
6784
- await generateSharedComponent2(projectRoot, {
6785
- name: "Footer",
6786
- type: "layout",
6787
- code: footerCode,
6788
- description: "Site footer",
6789
- usedIn: ["app/layout.tsx"],
6790
- overwrite: true
6791
- });
6792
- }
6793
- if (navType === "sidebar" || navType === "both") {
6794
- const sidebarCode = generator.generateSharedSidebarCode();
6795
- await generateSharedComponent2(projectRoot, {
6796
- name: "Sidebar",
6797
- type: "layout",
6798
- code: sidebarCode,
6799
- description: "Vertical sidebar navigation with collapsible sections",
6800
- usedIn: ["app/(app)/layout.tsx"],
6801
- overwrite: true
6802
- });
6958
+ const shouldRegenShared = !initialized || options.navChanged;
6959
+ if (shouldRegenShared) {
6960
+ if (navType === "header" || navType === "both") {
6961
+ if (await canOverwriteShared(projectRoot, "components/shared/header.tsx", hashes)) {
6962
+ const headerCode = generator.generateSharedHeaderCode();
6963
+ await generateSharedComponent2(projectRoot, {
6964
+ name: "Header",
6965
+ type: "layout",
6966
+ code: headerCode,
6967
+ description: "Main site header with navigation and theme toggle",
6968
+ usedIn: ["app/layout.tsx"],
6969
+ overwrite: true
6970
+ });
6971
+ }
6972
+ }
6973
+ if (await canOverwriteShared(projectRoot, "components/shared/footer.tsx", hashes)) {
6974
+ const footerCode = generator.generateSharedFooterCode();
6975
+ await generateSharedComponent2(projectRoot, {
6976
+ name: "Footer",
6977
+ type: "layout",
6978
+ code: footerCode,
6979
+ description: "Site footer",
6980
+ usedIn: ["app/layout.tsx"],
6981
+ overwrite: true
6982
+ });
6983
+ }
6984
+ if (navType === "sidebar" || navType === "both") {
6985
+ if (await canOverwriteShared(projectRoot, "components/shared/sidebar.tsx", hashes)) {
6986
+ const sidebarCode = generator.generateSharedSidebarCode();
6987
+ await generateSharedComponent2(projectRoot, {
6988
+ name: "Sidebar",
6989
+ type: "layout",
6990
+ code: sidebarCode,
6991
+ description: "Vertical sidebar navigation with collapsible sections",
6992
+ usedIn: ["app/(app)/layout.tsx"],
6993
+ overwrite: true
6994
+ });
6995
+ }
6996
+ }
6803
6997
  }
6804
6998
  }
6805
6999
  try {
@@ -6854,7 +7048,7 @@ export default function AppLayout({
6854
7048
  }
6855
7049
  `;
6856
7050
  }
6857
- async function regenerateFiles(modified, config2, projectRoot) {
7051
+ async function regenerateFiles(modified, config2, projectRoot, options = { navChanged: false }) {
6858
7052
  const componentIds = /* @__PURE__ */ new Set();
6859
7053
  const pageIds = /* @__PURE__ */ new Set();
6860
7054
  for (const item of modified) {
@@ -6865,7 +7059,10 @@ async function regenerateFiles(modified, config2, projectRoot) {
6865
7059
  }
6866
7060
  }
6867
7061
  if (config2.navigation?.enabled && modified.length > 0) {
6868
- await regenerateLayout(config2, projectRoot);
7062
+ await regenerateLayout(config2, projectRoot, {
7063
+ navChanged: options.navChanged,
7064
+ storedHashes: options.storedHashes
7065
+ });
6869
7066
  }
6870
7067
  if (componentIds.size > 0) {
6871
7068
  const twGen = new TailwindConfigGenerator(config2);
@@ -7592,7 +7789,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7592
7789
  await ensureAuthRouteGroup(projectRoot);
7593
7790
  }
7594
7791
  const filePath = routeToFsPath(projectRoot, route, isAuth);
7595
- await mkdir3(dirname6(filePath), { recursive: true });
7792
+ await mkdir4(dirname6(filePath), { recursive: true });
7596
7793
  const { fixedCode, fixes: postFixes } = await validateAndFixGeneratedCode(projectRoot, finalPageCode, {
7597
7794
  isPage: true
7598
7795
  });
@@ -7642,10 +7839,9 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7642
7839
  layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout"),
7643
7840
  allShared: manifestForAudit.shared
7644
7841
  });
7645
- const validRoutes = dsm.getConfig().pages.map((p) => p.route);
7646
- const issues = validatePageQuality(codeToWrite, validRoutes);
7842
+ const issues = validatePageQuality(codeToWrite);
7647
7843
  const errors = issues.filter((i) => i.severity === "error");
7648
- if (errors.length >= 5 && aiProvider) {
7844
+ if (errors.length >= 2 && aiProvider) {
7649
7845
  console.log(
7650
7846
  chalk11.yellow(`
7651
7847
  \u{1F504} ${errors.length} quality errors \u2014 attempting AI fix for ${page.name || page.id}...`)
@@ -7664,7 +7860,7 @@ Rules:
7664
7860
  - Keep all existing functionality and layout intact`;
7665
7861
  const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
7666
7862
  if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
7667
- const recheck = validatePageQuality(fixedCode2, validRoutes);
7863
+ const recheck = validatePageQuality(fixedCode2);
7668
7864
  const recheckErrors = recheck.filter((i) => i.severity === "error");
7669
7865
  if (recheckErrors.length < errors.length) {
7670
7866
  codeToWrite = fixedCode2;
@@ -7682,6 +7878,12 @@ Rules:
7682
7878
  \u{1F50D} Quality check for ${page.name || page.id}:`));
7683
7879
  console.log(chalk11.dim(report));
7684
7880
  }
7881
+ const consistency = checkDesignConsistency(codeToWrite);
7882
+ if (consistency.length > 0) {
7883
+ console.log(chalk11.yellow(`
7884
+ \u{1F3A8} Design consistency for ${page.name || page.id}:`));
7885
+ consistency.forEach((w) => console.log(chalk11.dim(` \u26A0 [${w.type}] ${w.message}`)));
7886
+ }
7685
7887
  }
7686
7888
  }
7687
7889
  return {
@@ -7747,6 +7949,12 @@ ${routeRules}
7747
7949
  ${pagesCtx}`
7748
7950
  );
7749
7951
  if (DEBUG2) console.log(chalk11.dim(` [update-page] AI returned ${resolvedPageCode.length} chars`));
7952
+ const editIssues = verifyIncrementalEdit(currentCode, resolvedPageCode);
7953
+ if (editIssues.length > 0) {
7954
+ console.log(chalk11.yellow(`
7955
+ \u26A0 Incremental edit issues for ${pageDef.name || pageDef.id}:`));
7956
+ editIssues.forEach((issue) => console.log(chalk11.dim(` [${issue.type}] ${issue.message}`)));
7957
+ }
7750
7958
  } else {
7751
7959
  console.log(chalk11.yellow(" \u26A0 AI provider does not support editPageCode"));
7752
7960
  }
@@ -7771,7 +7979,7 @@ ${pagesCtx}`
7771
7979
  if (installed.length > 0) {
7772
7980
  result.modified = [...result.modified, ...installed.map((id) => `component:${id}`)];
7773
7981
  }
7774
- await mkdir3(dirname6(absPath), { recursive: true });
7982
+ await mkdir4(dirname6(absPath), { recursive: true });
7775
7983
  const { fixedCode, fixes: postFixes } = await validateAndFixGeneratedCode(projectRoot, resolvedPageCode, {
7776
7984
  isPage: true
7777
7985
  });
@@ -7833,6 +8041,12 @@ ${pagesCtx}`
7833
8041
  \u{1F50D} Quality check for ${pageDef.name || pageDef.id}:`));
7834
8042
  console.log(chalk11.dim(report));
7835
8043
  }
8044
+ const consistency = checkDesignConsistency(codeToWrite);
8045
+ if (consistency.length > 0) {
8046
+ console.log(chalk11.yellow(`
8047
+ \u{1F3A8} Design consistency for ${pageDef.name || pageDef.id}:`));
8048
+ consistency.forEach((w) => console.log(chalk11.dim(` \u26A0 [${w.type}] ${w.message}`)));
8049
+ }
7836
8050
  } else {
7837
8051
  try {
7838
8052
  let code = await readFile(absPath);
@@ -7889,6 +8103,15 @@ ${pagesCtx}`
7889
8103
  }
7890
8104
  }
7891
8105
 
8106
+ // src/utils/nav-snapshot.ts
8107
+ function takeNavSnapshot(items) {
8108
+ if (!items || items.length === 0) return "[]";
8109
+ return JSON.stringify(items.map((i) => `${i.label}:${i.href}`).sort());
8110
+ }
8111
+ function hasNavChanged(before, after) {
8112
+ return before !== after;
8113
+ }
8114
+
7892
8115
  // src/commands/chat/interactive.ts
7893
8116
  import chalk12 from "chalk";
7894
8117
  import { resolve as resolve8 } from "path";
@@ -8121,6 +8344,7 @@ async function chatCommand(message, options) {
8121
8344
  }
8122
8345
  spinner.text = "Loading design system configuration...";
8123
8346
  }
8347
+ const storedHashes = await loadHashes(projectRoot);
8124
8348
  const dsm = new DesignSystemManager7(configPath);
8125
8349
  await dsm.load();
8126
8350
  const cm = new ComponentManager4(config2);
@@ -8324,18 +8548,18 @@ async function chatCommand(message, options) {
8324
8548
  );
8325
8549
  const preflightInstalledIds = [];
8326
8550
  const allNpmImportsFromPages = /* @__PURE__ */ new Set();
8551
+ const allNeededComponentIds = /* @__PURE__ */ new Set();
8327
8552
  for (const pageRequest of pageRequests) {
8328
8553
  const page = pageRequest.changes;
8329
- const neededComponentIds = /* @__PURE__ */ new Set();
8330
8554
  page.sections?.forEach(
8331
8555
  (section) => {
8332
8556
  if (section.componentId) {
8333
- neededComponentIds.add(section.componentId);
8557
+ allNeededComponentIds.add(section.componentId);
8334
8558
  }
8335
8559
  if (section.props?.fields && Array.isArray(section.props.fields)) {
8336
8560
  section.props.fields.forEach((field) => {
8337
8561
  if (field.component) {
8338
- neededComponentIds.add(field.component);
8562
+ allNeededComponentIds.add(field.component);
8339
8563
  }
8340
8564
  });
8341
8565
  }
@@ -8344,7 +8568,7 @@ async function chatCommand(message, options) {
8344
8568
  if (typeof page.pageCode === "string" && page.pageCode.trim() !== "") {
8345
8569
  const importMatches = page.pageCode.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
8346
8570
  for (const m of importMatches) {
8347
- if (m[1]) neededComponentIds.add(m[1]);
8571
+ if (m[1]) allNeededComponentIds.add(m[1]);
8348
8572
  }
8349
8573
  extractNpmPackagesFromCode(page.pageCode).forEach((p) => allNpmImportsFromPages.add(p));
8350
8574
  }
@@ -8359,7 +8583,7 @@ async function chatCommand(message, options) {
8359
8583
  });
8360
8584
  const tmplImports = preview.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
8361
8585
  for (const m of tmplImports) {
8362
- if (m[1]) neededComponentIds.add(m[1]);
8586
+ if (m[1]) allNeededComponentIds.add(m[1]);
8363
8587
  }
8364
8588
  extractNpmPackagesFromCode(preview).forEach((p) => allNpmImportsFromPages.add(p));
8365
8589
  } catch {
@@ -8367,8 +8591,8 @@ async function chatCommand(message, options) {
8367
8591
  }
8368
8592
  }
8369
8593
  if (DEBUG4) {
8370
- console.log(chalk13.gray("\n[DEBUG] Pre-flight analysis:"));
8371
- console.log(chalk13.gray(` Needed components: ${Array.from(neededComponentIds).join(", ")}`));
8594
+ console.log(chalk13.gray(`
8595
+ [DEBUG] Pre-flight analysis for page "${page.name || page.route}": `));
8372
8596
  console.log(chalk13.gray(` Page sections: ${page.sections?.length || 0}`));
8373
8597
  if (page.sections?.[0]?.props?.fields) {
8374
8598
  console.log(chalk13.gray(` First section has ${page.sections[0].props.fields.length} fields`));
@@ -8376,64 +8600,67 @@ async function chatCommand(message, options) {
8376
8600
  console.log(chalk13.gray(` Field ${i}: component=${f.component}`));
8377
8601
  });
8378
8602
  }
8379
- console.log("");
8380
8603
  }
8381
- const INVALID_COMPONENT_IDS = /* @__PURE__ */ new Set(["ui", "shared", "lib", "utils", "hooks", "app", "components"]);
8382
- for (const id of INVALID_COMPONENT_IDS) neededComponentIds.delete(id);
8383
- const missingComponents = [];
8384
- for (const componentId of neededComponentIds) {
8385
- const exists = cm.read(componentId);
8386
- if (DEBUG4) console.log(chalk13.gray(` Checking ${componentId}: ${exists ? "EXISTS" : "MISSING"}`));
8387
- if (!exists) {
8388
- missingComponents.push(componentId);
8389
- }
8604
+ }
8605
+ const INVALID_COMPONENT_IDS = /* @__PURE__ */ new Set(["ui", "shared", "lib", "utils", "hooks", "app", "components"]);
8606
+ for (const id of INVALID_COMPONENT_IDS) allNeededComponentIds.delete(id);
8607
+ if (DEBUG4) {
8608
+ console.log(chalk13.gray("\n[DEBUG] Pre-flight analysis (consolidated):"));
8609
+ console.log(chalk13.gray(` All needed components: ${Array.from(allNeededComponentIds).join(", ")}`));
8610
+ console.log("");
8611
+ }
8612
+ const missingComponents = [];
8613
+ for (const componentId of allNeededComponentIds) {
8614
+ const exists = cm.read(componentId);
8615
+ if (DEBUG4) console.log(chalk13.gray(` Checking ${componentId}: ${exists ? "EXISTS" : "MISSING"}`));
8616
+ if (!exists) {
8617
+ missingComponents.push(componentId);
8390
8618
  }
8391
- if (missingComponents.length > 0) {
8392
- spinner.stop();
8393
- console.log(chalk13.cyan("\n\u{1F50D} Pre-flight check: Installing missing components...\n"));
8394
- for (const componentId of missingComponents) {
8395
- if (DEBUG4) {
8396
- console.log(chalk13.gray(` [DEBUG] Trying to install: ${componentId}`));
8397
- console.log(chalk13.gray(` [DEBUG] isShadcnComponent(${componentId}): ${isShadcnComponent(componentId)}`));
8398
- }
8399
- if (isShadcnComponent(componentId)) {
8400
- try {
8401
- const shadcnDef = await installShadcnComponent(componentId, projectRoot);
8402
- if (DEBUG4)
8403
- console.log(chalk13.gray(` [DEBUG] shadcnDef for ${componentId}: ${shadcnDef ? "OK" : "NULL"}`));
8404
- if (shadcnDef) {
8405
- if (DEBUG4) console.log(chalk13.gray(` [DEBUG] Registering ${shadcnDef.id} (${shadcnDef.name})`));
8406
- const result = await cm.register(shadcnDef);
8407
- if (DEBUG4) {
8408
- console.log(
8409
- chalk13.gray(
8410
- ` [DEBUG] Register result: ${result.success ? "SUCCESS" : "FAILED"}${!result.success && result.message ? ` - ${result.message}` : ""}`
8411
- )
8412
- );
8413
- }
8414
- if (result.success) {
8415
- preflightInstalledIds.push(shadcnDef.id);
8416
- console.log(chalk13.green(` \u2728 Auto-installed ${shadcnDef.name} component`));
8417
- const updatedConfig2 = result.config;
8418
- dsm.updateConfig(updatedConfig2);
8419
- cm.updateConfig(updatedConfig2);
8420
- pm.updateConfig(updatedConfig2);
8421
- }
8619
+ }
8620
+ if (missingComponents.length > 0) {
8621
+ spinner.stop();
8622
+ console.log(chalk13.cyan("\n\u{1F50D} Pre-flight check: Installing missing components...\n"));
8623
+ for (const componentId of missingComponents) {
8624
+ if (DEBUG4) {
8625
+ console.log(chalk13.gray(` [DEBUG] Trying to install: ${componentId}`));
8626
+ console.log(chalk13.gray(` [DEBUG] isShadcnComponent(${componentId}): ${isShadcnComponent(componentId)}`));
8627
+ }
8628
+ if (isShadcnComponent(componentId)) {
8629
+ try {
8630
+ const shadcnDef = await installShadcnComponent(componentId, projectRoot);
8631
+ if (DEBUG4) console.log(chalk13.gray(` [DEBUG] shadcnDef for ${componentId}: ${shadcnDef ? "OK" : "NULL"}`));
8632
+ if (shadcnDef) {
8633
+ if (DEBUG4) console.log(chalk13.gray(` [DEBUG] Registering ${shadcnDef.id} (${shadcnDef.name})`));
8634
+ const result = await cm.register(shadcnDef);
8635
+ if (DEBUG4) {
8636
+ console.log(
8637
+ chalk13.gray(
8638
+ ` [DEBUG] Register result: ${result.success ? "SUCCESS" : "FAILED"}${!result.success && result.message ? ` - ${result.message}` : ""}`
8639
+ )
8640
+ );
8422
8641
  }
8423
- } catch (error) {
8424
- console.log(chalk13.red(` \u274C Failed to install ${componentId}:`));
8425
- console.log(chalk13.red(` ${error instanceof Error ? error.message : error}`));
8426
- if (error instanceof Error && error.stack) {
8427
- console.log(chalk13.gray(` ${error.stack.split("\n")[1]}`));
8642
+ if (result.success) {
8643
+ preflightInstalledIds.push(shadcnDef.id);
8644
+ console.log(chalk13.green(` \u2728 Auto-installed ${shadcnDef.name} component`));
8645
+ const updatedConfig2 = result.config;
8646
+ dsm.updateConfig(updatedConfig2);
8647
+ cm.updateConfig(updatedConfig2);
8648
+ pm.updateConfig(updatedConfig2);
8428
8649
  }
8429
8650
  }
8430
- } else {
8431
- console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
8651
+ } catch (error) {
8652
+ console.log(chalk13.red(` \u274C Failed to install ${componentId}:`));
8653
+ console.log(chalk13.red(` ${error instanceof Error ? error.message : error}`));
8654
+ if (error instanceof Error && error.stack) {
8655
+ console.log(chalk13.gray(` ${error.stack.split("\n")[1]}`));
8656
+ }
8432
8657
  }
8658
+ } else {
8659
+ console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
8433
8660
  }
8434
- console.log("");
8435
- spinner.start("Applying modifications...");
8436
8661
  }
8662
+ console.log("");
8663
+ spinner.start("Applying modifications...");
8437
8664
  }
8438
8665
  const installedPkgs = getInstalledPackages(projectRoot);
8439
8666
  const neededPkgs = /* @__PURE__ */ new Set([...COHERENT_REQUIRED_PACKAGES, ...allNpmImportsFromPages]);
@@ -8471,6 +8698,9 @@ async function chatCommand(message, options) {
8471
8698
  if (DEBUG4) console.log(chalk13.dim("[backup] Created snapshot"));
8472
8699
  } catch {
8473
8700
  }
8701
+ const navBefore = takeNavSnapshot(
8702
+ config2.navigation?.items?.map((i) => ({ label: i.label, href: i.route || `/${i.label.toLowerCase()}` }))
8703
+ );
8474
8704
  spinner.start("Applying modifications...");
8475
8705
  const results = [];
8476
8706
  for (const request of normalizedRequests) {
@@ -8592,6 +8822,34 @@ async function chatCommand(message, options) {
8592
8822
  spinner.start("Finalizing...");
8593
8823
  }
8594
8824
  }
8825
+ const finalConfig = dsm.getConfig();
8826
+ const allRoutes = finalConfig.pages.map((p) => p.route).filter(Boolean);
8827
+ if (allRoutes.length > 1) {
8828
+ const linkIssues = [];
8829
+ for (const result of results) {
8830
+ if (!result.success) continue;
8831
+ for (const mod of result.modified) {
8832
+ if (mod.startsWith("app/") && mod.endsWith("/page.tsx")) {
8833
+ try {
8834
+ const code = readFileSync10(resolve9(projectRoot, mod), "utf-8");
8835
+ const issues = validatePageQuality(code, allRoutes).filter(
8836
+ (i) => i.type === "BROKEN_INTERNAL_LINK"
8837
+ );
8838
+ for (const issue of issues) {
8839
+ linkIssues.push({ page: mod, message: issue.message });
8840
+ }
8841
+ } catch {
8842
+ }
8843
+ }
8844
+ }
8845
+ }
8846
+ if (linkIssues.length > 0) {
8847
+ console.log(chalk13.yellow("\n\u{1F517} Broken internal links:"));
8848
+ for (const { page, message: message2 } of linkIssues) {
8849
+ console.log(chalk13.dim(` ${page}: ${message2}`));
8850
+ }
8851
+ }
8852
+ }
8595
8853
  const updatedConfig = dsm.getConfig();
8596
8854
  const darkMatch = /\bdark\s*(theme|mode|background)\b/i.test(message);
8597
8855
  const lightMatch = /\blight\s*(theme|mode|background)\b/i.test(message);
@@ -8618,6 +8876,10 @@ async function chatCommand(message, options) {
8618
8876
  } catch {
8619
8877
  }
8620
8878
  }
8879
+ if (updatedConfig.settings.initialized === false) {
8880
+ updatedConfig.settings.initialized = true;
8881
+ dsm.updateConfig(updatedConfig);
8882
+ }
8621
8883
  spinner.text = "Saving configuration...";
8622
8884
  await dsm.save();
8623
8885
  spinner.succeed("Configuration saved");
@@ -8627,15 +8889,42 @@ async function chatCommand(message, options) {
8627
8889
  scaffoldedPages.forEach(({ route }) => {
8628
8890
  allModified.add(`page:${route.slice(1) || "home"}`);
8629
8891
  });
8892
+ const navAfter = takeNavSnapshot(
8893
+ updatedConfig.navigation?.items?.map((i) => ({
8894
+ label: i.label,
8895
+ href: i.route || `/${i.label.toLowerCase()}`
8896
+ }))
8897
+ );
8898
+ const navChanged = hasNavChanged(navBefore, navAfter);
8630
8899
  if (allModified.size > 0) {
8631
8900
  spinner.start("Regenerating affected files...");
8632
- await regenerateFiles(Array.from(allModified), updatedConfig, projectRoot);
8901
+ await regenerateFiles(Array.from(allModified), updatedConfig, projectRoot, { navChanged, storedHashes });
8633
8902
  spinner.succeed("Files regenerated");
8634
8903
  }
8635
8904
  try {
8636
8905
  fixGlobalsCss(projectRoot, updatedConfig);
8637
8906
  } catch {
8638
8907
  }
8908
+ try {
8909
+ const updatedHashes = { ...storedHashes };
8910
+ const sharedDir = resolve9(projectRoot, "components", "shared");
8911
+ const layoutFile = resolve9(projectRoot, "app", "layout.tsx");
8912
+ const filesToHash = [layoutFile];
8913
+ if (existsSync16(sharedDir)) {
8914
+ for (const f of readdirSync2(sharedDir)) {
8915
+ if (f.endsWith(".tsx")) filesToHash.push(resolve9(sharedDir, f));
8916
+ }
8917
+ }
8918
+ for (const filePath of filesToHash) {
8919
+ if (existsSync16(filePath)) {
8920
+ const rel = relative2(projectRoot, filePath);
8921
+ updatedHashes[rel] = await computeFileHash(filePath);
8922
+ }
8923
+ }
8924
+ await saveHashes(projectRoot, updatedHashes);
8925
+ } catch {
8926
+ if (DEBUG4) console.log(chalk13.dim("[hashes] Could not save file hashes"));
8927
+ }
8639
8928
  const successfulPairs = normalizedRequests.map((request, index) => ({ request, result: results[index] })).filter(({ result }) => result.success);
8640
8929
  if (successfulPairs.length > 0) {
8641
8930
  const changes = successfulPairs.map(({ request }) => ({
@@ -8743,18 +9032,18 @@ import chalk14 from "chalk";
8743
9032
  import ora3 from "ora";
8744
9033
  import { spawn } from "child_process";
8745
9034
  import { existsSync as existsSync19, rmSync as rmSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
8746
- import { resolve as resolve10, join as join11 } from "path";
9035
+ import { resolve as resolve10, join as join12 } from "path";
8747
9036
  import { readdir as readdir2 } from "fs/promises";
8748
9037
  import { DesignSystemManager as DesignSystemManager8, ComponentGenerator as ComponentGenerator3 } from "@getcoherent/core";
8749
9038
 
8750
9039
  // src/utils/file-watcher.ts
8751
9040
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync18 } from "fs";
8752
- import { relative as relative3, join as join10 } from "path";
9041
+ import { relative as relative4, join as join11 } from "path";
8753
9042
  import { loadManifest as loadManifest8, saveManifest as saveManifest3 } from "@getcoherent/core";
8754
9043
 
8755
9044
  // src/utils/component-integrity.ts
8756
- import { existsSync as existsSync17, readFileSync as readFileSync11, readdirSync as readdirSync2 } from "fs";
8757
- import { join as join9, relative as relative2 } from "path";
9045
+ import { existsSync as existsSync17, readFileSync as readFileSync11, readdirSync as readdirSync3 } from "fs";
9046
+ import { join as join10, relative as relative3 } from "path";
8758
9047
  function extractExportedComponentNames(code) {
8759
9048
  const names = [];
8760
9049
  let m;
@@ -8780,7 +9069,7 @@ function arraysEqual(a, b) {
8780
9069
  }
8781
9070
  function findPagesImporting(projectRoot, componentName, componentFile) {
8782
9071
  const results = [];
8783
- const appDir = join9(projectRoot, "app");
9072
+ const appDir = join10(projectRoot, "app");
8784
9073
  if (!existsSync17(appDir)) return results;
8785
9074
  const pageFiles = collectFiles(appDir, (name) => name === "page.tsx" || name === "page.jsx");
8786
9075
  const componentImportPath = componentFile.replace(/\.tsx$/, "").replace(/\.jsx$/, "");
@@ -8792,7 +9081,7 @@ function findPagesImporting(projectRoot, componentName, componentFile) {
8792
9081
  const hasDefaultImport = new RegExp(`import\\s+${componentName}\\s+from\\s+['"]`).test(code);
8793
9082
  const hasPathImport = code.includes(`@/${componentImportPath}`);
8794
9083
  if (hasNamedImport || hasDefaultImport || hasPathImport) {
8795
- results.push(relative2(projectRoot, absPath));
9084
+ results.push(relative3(projectRoot, absPath));
8796
9085
  }
8797
9086
  } catch {
8798
9087
  }
@@ -8800,7 +9089,7 @@ function findPagesImporting(projectRoot, componentName, componentFile) {
8800
9089
  return results;
8801
9090
  }
8802
9091
  function isUsedInLayout(projectRoot, componentName) {
8803
- const layoutPath = join9(projectRoot, "app", "layout.tsx");
9092
+ const layoutPath = join10(projectRoot, "app", "layout.tsx");
8804
9093
  if (!existsSync17(layoutPath)) return false;
8805
9094
  try {
8806
9095
  const code = readFileSync11(layoutPath, "utf-8");
@@ -8811,7 +9100,7 @@ function isUsedInLayout(projectRoot, componentName) {
8811
9100
  }
8812
9101
  function findUnregisteredComponents(projectRoot, manifest) {
8813
9102
  const results = [];
8814
- const componentsDir = join9(projectRoot, "components");
9103
+ const componentsDir = join10(projectRoot, "components");
8815
9104
  if (!existsSync17(componentsDir)) return results;
8816
9105
  const registeredFiles = new Set(manifest.shared.map((s) => s.file));
8817
9106
  const registeredNames = new Set(manifest.shared.map((s) => s.name));
@@ -8821,7 +9110,7 @@ function findUnregisteredComponents(projectRoot, manifest) {
8821
9110
  ["ui", "node_modules"]
8822
9111
  );
8823
9112
  for (const absPath of files) {
8824
- const relFile = relative2(projectRoot, absPath);
9113
+ const relFile = relative3(projectRoot, absPath);
8825
9114
  if (registeredFiles.has(relFile)) continue;
8826
9115
  try {
8827
9116
  const code = readFileSync11(absPath, "utf-8");
@@ -8839,7 +9128,7 @@ function findUnregisteredComponents(projectRoot, manifest) {
8839
9128
  }
8840
9129
  function findInlineDuplicates(projectRoot, manifest) {
8841
9130
  const results = [];
8842
- const appDir = join9(projectRoot, "app");
9131
+ const appDir = join10(projectRoot, "app");
8843
9132
  if (!existsSync17(appDir)) return results;
8844
9133
  const pageFiles = collectFiles(appDir, (name) => name === "page.tsx" || name === "page.jsx");
8845
9134
  for (const absPath of pageFiles) {
@@ -8850,7 +9139,7 @@ function findInlineDuplicates(projectRoot, manifest) {
8850
9139
  } catch {
8851
9140
  continue;
8852
9141
  }
8853
- const relPath = relative2(projectRoot, absPath);
9142
+ const relPath = relative3(projectRoot, absPath);
8854
9143
  for (const shared of manifest.shared) {
8855
9144
  const importPath = shared.file.replace(/\.tsx$/, "").replace(/\.jsx$/, "");
8856
9145
  const isImported = code.includes(`@/${importPath}`) || code.includes(`from './${importPath}'`) || code.includes(`from "../${importPath}"`);
@@ -8869,7 +9158,7 @@ function findInlineDuplicates(projectRoot, manifest) {
8869
9158
  return results;
8870
9159
  }
8871
9160
  function findComponentFileByExportName(projectRoot, componentName) {
8872
- const componentsDir = join9(projectRoot, "components");
9161
+ const componentsDir = join10(projectRoot, "components");
8873
9162
  if (!existsSync17(componentsDir)) return null;
8874
9163
  const files = collectFiles(
8875
9164
  componentsDir,
@@ -8881,7 +9170,7 @@ function findComponentFileByExportName(projectRoot, componentName) {
8881
9170
  const code = readFileSync11(absPath, "utf-8");
8882
9171
  const exports = extractExportedComponentNames(code);
8883
9172
  if (exports.includes(componentName)) {
8884
- return relative2(projectRoot, absPath);
9173
+ return relative3(projectRoot, absPath);
8885
9174
  }
8886
9175
  } catch {
8887
9176
  }
@@ -8891,7 +9180,7 @@ function findComponentFileByExportName(projectRoot, componentName) {
8891
9180
  function removeOrphanedEntries(projectRoot, manifest) {
8892
9181
  const removed = [];
8893
9182
  const valid = manifest.shared.filter((entry) => {
8894
- const filePath = join9(projectRoot, entry.file);
9183
+ const filePath = join10(projectRoot, entry.file);
8895
9184
  if (existsSync17(filePath)) return true;
8896
9185
  removed.push({ id: entry.id, name: entry.name });
8897
9186
  return false;
@@ -8910,7 +9199,7 @@ function reconcileComponents(projectRoot, manifest) {
8910
9199
  };
8911
9200
  const m = { ...manifest, shared: [...manifest.shared], nextId: manifest.nextId };
8912
9201
  m.shared = m.shared.filter((entry) => {
8913
- const filePath = join9(projectRoot, entry.file);
9202
+ const filePath = join10(projectRoot, entry.file);
8914
9203
  if (!existsSync17(filePath)) {
8915
9204
  const newPath = findComponentFileByExportName(projectRoot, entry.name);
8916
9205
  if (newPath) {
@@ -8923,7 +9212,7 @@ function reconcileComponents(projectRoot, manifest) {
8923
9212
  }
8924
9213
  let code;
8925
9214
  try {
8926
- code = readFileSync11(join9(projectRoot, entry.file), "utf-8");
9215
+ code = readFileSync11(join10(projectRoot, entry.file), "utf-8");
8927
9216
  } catch {
8928
9217
  return true;
8929
9218
  }
@@ -9000,12 +9289,12 @@ function collectFiles(dir, filter, skipDirs = []) {
9000
9289
  function walk(d) {
9001
9290
  let entries;
9002
9291
  try {
9003
- entries = readdirSync2(d, { withFileTypes: true });
9292
+ entries = readdirSync3(d, { withFileTypes: true });
9004
9293
  } catch {
9005
9294
  return;
9006
9295
  }
9007
9296
  for (const e of entries) {
9008
- const full = join9(d, e.name);
9297
+ const full = join10(d, e.name);
9009
9298
  if (e.isDirectory()) {
9010
9299
  if (skipDirs.includes(e.name) || e.name.startsWith(".")) continue;
9011
9300
  walk(full);
@@ -9044,7 +9333,7 @@ function findInlineDuplicatesOfShared(content, manifest) {
9044
9333
  }
9045
9334
  function getWatcherConfig(projectRoot) {
9046
9335
  try {
9047
- const pkgPath = join10(projectRoot, "package.json");
9336
+ const pkgPath = join11(projectRoot, "package.json");
9048
9337
  if (!existsSync18(pkgPath)) return defaultWatcherConfig();
9049
9338
  const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
9050
9339
  const c = pkg?.coherent?.watcher ?? {};
@@ -9069,7 +9358,7 @@ function defaultWatcherConfig() {
9069
9358
  };
9070
9359
  }
9071
9360
  async function handleFileChange(projectRoot, filePath) {
9072
- const relativePath = relative3(projectRoot, filePath).replace(/\\/g, "/");
9361
+ const relativePath = relative4(projectRoot, filePath).replace(/\\/g, "/");
9073
9362
  if (!relativePath.endsWith(".tsx") && !relativePath.endsWith(".ts")) return;
9074
9363
  if (relativePath.includes("node_modules") || relativePath.includes(".next")) return;
9075
9364
  let content;
@@ -9119,7 +9408,7 @@ async function handleFileChange(projectRoot, filePath) {
9119
9408
  }
9120
9409
  }
9121
9410
  async function handleFileDelete(projectRoot, filePath) {
9122
- const relativePath = relative3(projectRoot, filePath).replace(/\\/g, "/");
9411
+ const relativePath = relative4(projectRoot, filePath).replace(/\\/g, "/");
9123
9412
  if (!relativePath.startsWith("components/") || relativePath.startsWith("components/ui/")) return;
9124
9413
  try {
9125
9414
  const chalk32 = (await import("chalk")).default;
@@ -9139,7 +9428,7 @@ async function handleFileDelete(projectRoot, filePath) {
9139
9428
  }
9140
9429
  }
9141
9430
  async function detectNewComponent(projectRoot, filePath) {
9142
- const relativePath = relative3(projectRoot, filePath).replace(/\\/g, "/");
9431
+ const relativePath = relative4(projectRoot, filePath).replace(/\\/g, "/");
9143
9432
  if (!relativePath.startsWith("components/") || relativePath.startsWith("components/ui/")) return;
9144
9433
  if (!relativePath.endsWith(".tsx") && !relativePath.endsWith(".jsx")) return;
9145
9434
  try {
@@ -9173,8 +9462,8 @@ function startFileWatcher(projectRoot) {
9173
9462
  let watcher = null;
9174
9463
  let manifestWatcher = null;
9175
9464
  import("chokidar").then((chokidar) => {
9176
- const appGlob = join10(projectRoot, "app", "**", "*.tsx");
9177
- const compGlob = join10(projectRoot, "components", "**", "*.tsx");
9465
+ const appGlob = join11(projectRoot, "app", "**", "*.tsx");
9466
+ const compGlob = join11(projectRoot, "components", "**", "*.tsx");
9178
9467
  watcher = chokidar.default.watch([appGlob, compGlob], {
9179
9468
  ignoreInitial: true,
9180
9469
  awaitWriteFinish: { stabilityThreshold: 500 }
@@ -9186,7 +9475,7 @@ function startFileWatcher(projectRoot) {
9186
9475
  });
9187
9476
  watcher.on("unlink", (fp) => handleFileDelete(projectRoot, fp));
9188
9477
  });
9189
- const manifestPath = join10(projectRoot, "coherent.components.json");
9478
+ const manifestPath = join11(projectRoot, "coherent.components.json");
9190
9479
  if (existsSync18(manifestPath)) {
9191
9480
  import("chokidar").then((chokidar) => {
9192
9481
  manifestWatcher = chokidar.default.watch(manifestPath, { ignoreInitial: true });
@@ -9238,7 +9527,7 @@ function checkDependenciesInstalled(projectRoot) {
9238
9527
  return existsSync19(nodeModulesPath);
9239
9528
  }
9240
9529
  function clearStaleCache(projectRoot) {
9241
- const nextDir = join11(projectRoot, ".next");
9530
+ const nextDir = join12(projectRoot, ".next");
9242
9531
  if (existsSync19(nextDir)) {
9243
9532
  rmSync3(nextDir, { recursive: true, force: true });
9244
9533
  console.log(chalk14.dim(" \u2714 Cleared stale build cache"));
@@ -9259,7 +9548,7 @@ async function listPageFiles(appDir) {
9259
9548
  async function walk(dir) {
9260
9549
  const entries = await readdir2(dir, { withFileTypes: true });
9261
9550
  for (const e of entries) {
9262
- const full = join11(dir, e.name);
9551
+ const full = join12(dir, e.name);
9263
9552
  if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "api" && e.name !== "design-system") await walk(full);
9264
9553
  else if (e.isFile() && e.name === "page.tsx") out.push(full);
9265
9554
  }
@@ -9268,7 +9557,7 @@ async function listPageFiles(appDir) {
9268
9557
  return out;
9269
9558
  }
9270
9559
  async function validateSyntax(projectRoot) {
9271
- const appDir = join11(projectRoot, "app");
9560
+ const appDir = join12(projectRoot, "app");
9272
9561
  const pages = await listPageFiles(appDir);
9273
9562
  for (const file of pages) {
9274
9563
  const content = readFileSync13(file, "utf-8");
@@ -9280,8 +9569,8 @@ async function validateSyntax(projectRoot) {
9280
9569
  }
9281
9570
  }
9282
9571
  async function fixMissingComponentExports(projectRoot) {
9283
- const appDir = join11(projectRoot, "app");
9284
- const uiDir = join11(projectRoot, "components", "ui");
9572
+ const appDir = join12(projectRoot, "app");
9573
+ const uiDir = join12(projectRoot, "components", "ui");
9285
9574
  if (!existsSync19(appDir) || !existsSync19(uiDir)) return;
9286
9575
  const pages = await listPageFiles(appDir);
9287
9576
  const neededExports = /* @__PURE__ */ new Map();
@@ -9296,7 +9585,7 @@ async function fixMissingComponentExports(projectRoot) {
9296
9585
  for (const name of names) neededExports.get(componentId).add(name);
9297
9586
  }
9298
9587
  }
9299
- const configPath = join11(projectRoot, "design-system.config.ts");
9588
+ const configPath = join12(projectRoot, "design-system.config.ts");
9300
9589
  let config2 = null;
9301
9590
  try {
9302
9591
  const mgr = new DesignSystemManager8(configPath);
@@ -9305,7 +9594,7 @@ async function fixMissingComponentExports(projectRoot) {
9305
9594
  }
9306
9595
  const generator = new ComponentGenerator3(config2 || { components: [], pages: [], tokens: {} });
9307
9596
  for (const [componentId, needed] of neededExports) {
9308
- const componentFile = join11(uiDir, `${componentId}.tsx`);
9597
+ const componentFile = join12(uiDir, `${componentId}.tsx`);
9309
9598
  const def = getShadcnComponent(componentId);
9310
9599
  if (!existsSync19(componentFile)) {
9311
9600
  if (!def) continue;
@@ -9342,7 +9631,7 @@ async function fixMissingComponentExports(projectRoot) {
9342
9631
  }
9343
9632
  }
9344
9633
  async function backfillPageAnalysis(projectRoot) {
9345
- const configPath = join11(projectRoot, "design-system.config.ts");
9634
+ const configPath = join12(projectRoot, "design-system.config.ts");
9346
9635
  if (!existsSync19(configPath)) return;
9347
9636
  try {
9348
9637
  const mgr = new DesignSystemManager8(configPath);
@@ -9354,11 +9643,11 @@ async function backfillPageAnalysis(projectRoot) {
9354
9643
  const isAuth = route.includes("login") || route.includes("register") || route.includes("signup") || route.includes("sign-up");
9355
9644
  let filePath;
9356
9645
  if (route === "/") {
9357
- filePath = join11(projectRoot, "app", "page.tsx");
9646
+ filePath = join12(projectRoot, "app", "page.tsx");
9358
9647
  } else if (isAuth) {
9359
- filePath = join11(projectRoot, "app", "(auth)", route.slice(1), "page.tsx");
9648
+ filePath = join12(projectRoot, "app", "(auth)", route.slice(1), "page.tsx");
9360
9649
  } else {
9361
- filePath = join11(projectRoot, "app", route.slice(1), "page.tsx");
9650
+ filePath = join12(projectRoot, "app", route.slice(1), "page.tsx");
9362
9651
  }
9363
9652
  if (!existsSync19(filePath)) continue;
9364
9653
  const code = readFileSync13(filePath, "utf-8");
@@ -9388,7 +9677,7 @@ async function autoInstallShadcnComponent(componentId, projectRoot) {
9388
9677
  const def = getShadcnComponent(componentId);
9389
9678
  if (!def) return false;
9390
9679
  try {
9391
- const configPath = join11(projectRoot, "design-system.config.ts");
9680
+ const configPath = join12(projectRoot, "design-system.config.ts");
9392
9681
  let config2 = null;
9393
9682
  try {
9394
9683
  const mgr = new DesignSystemManager8(configPath);
@@ -9397,10 +9686,10 @@ async function autoInstallShadcnComponent(componentId, projectRoot) {
9397
9686
  }
9398
9687
  const generator = new ComponentGenerator3(config2 || { components: [], pages: [], tokens: {} });
9399
9688
  const code = await generator.generate(def);
9400
- const uiDir = join11(projectRoot, "components", "ui");
9689
+ const uiDir = join12(projectRoot, "components", "ui");
9401
9690
  const { mkdirSync: mkdirSync9 } = await import("fs");
9402
9691
  mkdirSync9(uiDir, { recursive: true });
9403
- writeFileSync10(join11(uiDir, `${componentId}.tsx`), code, "utf-8");
9692
+ writeFileSync10(join12(uiDir, `${componentId}.tsx`), code, "utf-8");
9404
9693
  return true;
9405
9694
  } catch {
9406
9695
  return false;
@@ -9614,9 +9903,9 @@ async function previewCommand() {
9614
9903
  import chalk15 from "chalk";
9615
9904
  import ora4 from "ora";
9616
9905
  import { spawn as spawn2 } from "child_process";
9617
- import { existsSync as existsSync20, rmSync as rmSync4, readdirSync as readdirSync3 } from "fs";
9618
- import { resolve as resolve11, join as join12, dirname as dirname7 } from "path";
9619
- import { readdir as readdir3, readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4, copyFile as copyFile2 } from "fs/promises";
9906
+ import { existsSync as existsSync20, rmSync as rmSync4, readdirSync as readdirSync4 } from "fs";
9907
+ import { resolve as resolve11, join as join13, dirname as dirname7 } from "path";
9908
+ import { readdir as readdir3, readFile as readFile6, writeFile as writeFile5, mkdir as mkdir5, copyFile as copyFile2 } from "fs/promises";
9620
9909
  var COPY_EXCLUDE = /* @__PURE__ */ new Set([
9621
9910
  "node_modules",
9622
9911
  ".next",
@@ -9634,16 +9923,16 @@ var COPY_EXCLUDE = /* @__PURE__ */ new Set([
9634
9923
  ".env.local"
9635
9924
  ]);
9636
9925
  async function copyDir(src, dest) {
9637
- await mkdir4(dest, { recursive: true });
9926
+ await mkdir5(dest, { recursive: true });
9638
9927
  const entries = await readdir3(src, { withFileTypes: true });
9639
9928
  for (const e of entries) {
9640
- const srcPath = join12(src, e.name);
9641
- const destPath = join12(dest, e.name);
9929
+ const srcPath = join13(src, e.name);
9930
+ const destPath = join13(dest, e.name);
9642
9931
  if (COPY_EXCLUDE.has(e.name)) continue;
9643
9932
  if (e.isDirectory()) {
9644
9933
  await copyDir(srcPath, destPath);
9645
9934
  } else {
9646
- await mkdir4(dirname7(destPath), { recursive: true });
9935
+ await mkdir5(dirname7(destPath), { recursive: true });
9647
9936
  await copyFile2(srcPath, destPath);
9648
9937
  }
9649
9938
  }
@@ -9658,7 +9947,7 @@ function getPackageManager2(projectRoot) {
9658
9947
  }
9659
9948
  async function patchNextConfigForExport(outRoot) {
9660
9949
  for (const name of ["next.config.ts", "next.config.mjs", "next.config.js"]) {
9661
- const p = join12(outRoot, name);
9950
+ const p = join13(outRoot, name);
9662
9951
  if (!existsSync20(p)) continue;
9663
9952
  let content = await readFile6(p, "utf-8");
9664
9953
  if (content.includes("ignoreDuringBuilds")) return;
@@ -9666,7 +9955,7 @@ async function patchNextConfigForExport(outRoot) {
9666
9955
  /(const\s+nextConfig\s*(?::\s*\w+)?\s*=\s*\{)/,
9667
9956
  "$1\n eslint: { ignoreDuringBuilds: true },\n typescript: { ignoreBuildErrors: true },"
9668
9957
  );
9669
- await writeFile4(p, content, "utf-8");
9958
+ await writeFile5(p, content, "utf-8");
9670
9959
  return;
9671
9960
  }
9672
9961
  }
@@ -9700,13 +9989,13 @@ EXPOSE 3000
9700
9989
  \`\`\`
9701
9990
  `;
9702
9991
  async function ensureReadmeDeploySection(outRoot) {
9703
- const readmePath = join12(outRoot, "README.md");
9992
+ const readmePath = join13(outRoot, "README.md");
9704
9993
  if (!existsSync20(readmePath)) return;
9705
9994
  try {
9706
9995
  let content = await readFile6(readmePath, "utf-8");
9707
9996
  if (/##\s+Deploy\b/m.test(content)) return;
9708
9997
  content = content.trimEnd() + DEPLOY_SECTION + "\n";
9709
- await writeFile4(readmePath, content);
9998
+ await writeFile5(readmePath, content);
9710
9999
  } catch {
9711
10000
  }
9712
10001
  }
@@ -9720,22 +10009,22 @@ async function countPages(outRoot) {
9720
10009
  return;
9721
10010
  }
9722
10011
  for (const e of entries) {
9723
- const full = join12(dir, e.name);
10012
+ const full = join13(dir, e.name);
9724
10013
  if (e.isFile() && e.name === "page.tsx") n++;
9725
10014
  else if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "api") await walk(full);
9726
10015
  }
9727
10016
  }
9728
- const appDir = join12(outRoot, "app");
10017
+ const appDir = join13(outRoot, "app");
9729
10018
  if (existsSync20(appDir)) await walk(appDir);
9730
10019
  return n;
9731
10020
  }
9732
10021
  function countComponents(outRoot) {
9733
10022
  let n = 0;
9734
10023
  for (const sub of ["ui", "shared"]) {
9735
- const dir = join12(outRoot, "components", sub);
10024
+ const dir = join13(outRoot, "components", sub);
9736
10025
  if (!existsSync20(dir)) continue;
9737
10026
  try {
9738
- n += readdirSync3(dir).filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx")).length;
10027
+ n += readdirSync4(dir).filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx")).length;
9739
10028
  } catch {
9740
10029
  }
9741
10030
  }
@@ -9748,7 +10037,7 @@ async function collectImportedPackages2(dir, extensions) {
9748
10037
  async function walk(d) {
9749
10038
  const entries = await readdir3(d, { withFileTypes: true });
9750
10039
  for (const e of entries) {
9751
- const full = join12(d, e.name);
10040
+ const full = join13(d, e.name);
9752
10041
  if (e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules") {
9753
10042
  await walk(full);
9754
10043
  continue;
@@ -9771,7 +10060,7 @@ async function collectImportedPackages2(dir, extensions) {
9771
10060
  return packages;
9772
10061
  }
9773
10062
  async function findMissingDepsInExport(outRoot) {
9774
- const pkgPath = join12(outRoot, "package.json");
10063
+ const pkgPath = join13(outRoot, "package.json");
9775
10064
  if (!existsSync20(pkgPath)) return [];
9776
10065
  let pkg;
9777
10066
  try {
@@ -9780,7 +10069,7 @@ async function findMissingDepsInExport(outRoot) {
9780
10069
  return [];
9781
10070
  }
9782
10071
  const inDeps = /* @__PURE__ */ new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
9783
- const codeDirs = [join12(outRoot, "app"), join12(outRoot, "components")];
10072
+ const codeDirs = [join13(outRoot, "app"), join13(outRoot, "components")];
9784
10073
  const extensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx"]);
9785
10074
  const imported = /* @__PURE__ */ new Set();
9786
10075
  for (const dir of codeDirs) {
@@ -9792,33 +10081,33 @@ async function findMissingDepsInExport(outRoot) {
9792
10081
  async function stripCoherentArtifacts(outputDir) {
9793
10082
  const removed = [];
9794
10083
  for (const p of ["app/design-system", "app/api/design-system"]) {
9795
- const full = join12(outputDir, p);
10084
+ const full = join13(outputDir, p);
9796
10085
  if (existsSync20(full)) {
9797
10086
  rmSync4(full, { recursive: true, force: true });
9798
10087
  removed.push(p);
9799
10088
  }
9800
10089
  }
9801
- const appNavPath = join12(outputDir, "app", "AppNav.tsx");
10090
+ const appNavPath = join13(outputDir, "app", "AppNav.tsx");
9802
10091
  if (existsSync20(appNavPath)) {
9803
10092
  rmSync4(appNavPath, { force: true });
9804
10093
  removed.push("app/AppNav.tsx");
9805
10094
  }
9806
- const layoutPath = join12(outputDir, "app", "layout.tsx");
10095
+ const layoutPath = join13(outputDir, "app", "layout.tsx");
9807
10096
  if (existsSync20(layoutPath)) {
9808
10097
  let layout = await readFile6(layoutPath, "utf-8");
9809
10098
  layout = layout.replace(/import\s*\{?\s*AppNav\s*\}?\s*from\s*['"][^'"]+['"]\s*\n?/g, "");
9810
10099
  layout = layout.replace(/\s*<AppNav\s*\/?\s*>\s*/g, "\n");
9811
- await writeFile4(layoutPath, layout, "utf-8");
10100
+ await writeFile5(layoutPath, layout, "utf-8");
9812
10101
  }
9813
- const sharedHeaderPath = join12(outputDir, "components", "shared", "header.tsx");
10102
+ const sharedHeaderPath = join13(outputDir, "components", "shared", "header.tsx");
9814
10103
  if (existsSync20(sharedHeaderPath)) {
9815
10104
  let header = await readFile6(sharedHeaderPath, "utf-8");
9816
10105
  header = header.replace(/<Link\s[^>]*href="\/design-system"[^>]*>[\s\S]*?<\/Link>/g, "");
9817
10106
  header = header.replace(/\n\s*<>\s*\n/, "\n");
9818
10107
  header = header.replace(/\n\s*<\/>\s*\n/, "\n");
9819
- await writeFile4(sharedHeaderPath, header, "utf-8");
10108
+ await writeFile5(sharedHeaderPath, header, "utf-8");
9820
10109
  }
9821
- const guardPath = join12(outputDir, "app", "ShowWhenNotAuthRoute.tsx");
10110
+ const guardPath = join13(outputDir, "app", "ShowWhenNotAuthRoute.tsx");
9822
10111
  if (existsSync20(guardPath)) {
9823
10112
  let guard = await readFile6(guardPath, "utf-8");
9824
10113
  guard = guard.replace(/['"],?\s*'\/design-system['"],?\s*/g, "");
@@ -9832,10 +10121,10 @@ async function stripCoherentArtifacts(outputDir) {
9832
10121
  layout = layout.replace(/import\s+\w+\s+from\s*['"]\.\/ShowWhenNotAuthRoute['"]\s*\n?/g, "");
9833
10122
  layout = layout.replace(/\s*<ShowWhenNotAuthRoute>\s*\n?/g, "\n");
9834
10123
  layout = layout.replace(/\s*<\/ShowWhenNotAuthRoute>\s*\n?/g, "\n");
9835
- await writeFile4(layoutPath, layout, "utf-8");
10124
+ await writeFile5(layoutPath, layout, "utf-8");
9836
10125
  }
9837
10126
  } else {
9838
- await writeFile4(guardPath, guard, "utf-8");
10127
+ await writeFile5(guardPath, guard, "utf-8");
9839
10128
  }
9840
10129
  }
9841
10130
  for (const name of [
@@ -9847,14 +10136,14 @@ async function stripCoherentArtifacts(outputDir) {
9847
10136
  ".env.local",
9848
10137
  "recommendations.md"
9849
10138
  ]) {
9850
- const full = join12(outputDir, name);
10139
+ const full = join13(outputDir, name);
9851
10140
  if (existsSync20(full)) {
9852
10141
  rmSync4(full, { force: true });
9853
10142
  removed.push(name);
9854
10143
  }
9855
10144
  }
9856
10145
  for (const dir of [".claude", ".coherent"]) {
9857
- const full = join12(outputDir, dir);
10146
+ const full = join13(outputDir, dir);
9858
10147
  if (existsSync20(full)) {
9859
10148
  rmSync4(full, { recursive: true, force: true });
9860
10149
  removed.push(dir + "/");
@@ -10054,8 +10343,8 @@ async function regenerateDocsCommand() {
10054
10343
 
10055
10344
  // src/commands/fix.ts
10056
10345
  import chalk18 from "chalk";
10057
- import { readdirSync as readdirSync4, readFileSync as readFileSync14, existsSync as existsSync21, writeFileSync as writeFileSync11, rmSync as rmSync5, mkdirSync as mkdirSync7 } from "fs";
10058
- import { resolve as resolve12, join as join13 } from "path";
10346
+ import { readdirSync as readdirSync5, readFileSync as readFileSync14, existsSync as existsSync21, writeFileSync as writeFileSync11, rmSync as rmSync5, mkdirSync as mkdirSync7 } from "fs";
10347
+ import { resolve as resolve12, join as join14 } from "path";
10059
10348
  import {
10060
10349
  DesignSystemManager as DesignSystemManager11,
10061
10350
  ComponentManager as ComponentManager5,
@@ -10079,9 +10368,9 @@ function extractComponentIdsFromCode2(code) {
10079
10368
  function listTsxFiles(dir) {
10080
10369
  const files = [];
10081
10370
  try {
10082
- const entries = readdirSync4(dir, { withFileTypes: true });
10371
+ const entries = readdirSync5(dir, { withFileTypes: true });
10083
10372
  for (const e of entries) {
10084
- const full = join13(dir, e.name);
10373
+ const full = join14(dir, e.name);
10085
10374
  if (e.isDirectory() && e.name !== "node_modules" && !e.name.startsWith(".")) {
10086
10375
  files.push(...listTsxFiles(full));
10087
10376
  } else if (e.isFile() && e.name.endsWith(".tsx")) {
@@ -10110,7 +10399,7 @@ async function fixCommand(opts = {}) {
10110
10399
  console.log(chalk18.cyan("\ncoherent fix\n"));
10111
10400
  }
10112
10401
  if (!skipCache) {
10113
- const nextDir = join13(projectRoot, ".next");
10402
+ const nextDir = join14(projectRoot, ".next");
10114
10403
  if (existsSync21(nextDir)) {
10115
10404
  if (!dryRun) rmSync5(nextDir, { recursive: true, force: true });
10116
10405
  fixes.push("Cleared build cache");
@@ -10367,13 +10656,13 @@ async function fixCommand(opts = {}) {
10367
10656
  // src/commands/check.ts
10368
10657
  import chalk19 from "chalk";
10369
10658
  import { resolve as resolve13 } from "path";
10370
- import { readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync2, existsSync as existsSync22 } from "fs";
10659
+ import { readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2, existsSync as existsSync22 } from "fs";
10371
10660
  import { loadManifest as loadManifest10 } from "@getcoherent/core";
10372
10661
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", "design-system"]);
10373
10662
  function findTsxFiles(dir) {
10374
10663
  const results = [];
10375
10664
  try {
10376
- const entries = readdirSync5(dir);
10665
+ const entries = readdirSync6(dir);
10377
10666
  for (const entry of entries) {
10378
10667
  const full = resolve13(dir, entry);
10379
10668
  const stat = statSync2(full);
@@ -10701,8 +10990,8 @@ import { existsSync as existsSync23 } from "fs";
10701
10990
  import { resolve as resolve14 } from "path";
10702
10991
 
10703
10992
  // src/utils/ds-files.ts
10704
- import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
10705
- import { join as join14, dirname as dirname8 } from "path";
10993
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
10994
+ import { join as join15, dirname as dirname8 } from "path";
10706
10995
  import { DesignSystemGenerator } from "@getcoherent/core";
10707
10996
  var SHARED_DS_KEYS = [
10708
10997
  "app/design-system/shared/page.tsx",
@@ -10716,9 +11005,9 @@ async function writeDesignSystemFiles(projectRoot, config2, options) {
10716
11005
  const toWrite = options?.sharedOnly ? new Map([...files].filter(([path3]) => SHARED_DS_KEYS.includes(path3))) : files;
10717
11006
  const written = [];
10718
11007
  for (const [relativePath, content] of toWrite) {
10719
- const fullPath = join14(projectRoot, relativePath);
10720
- await mkdir5(dirname8(fullPath), { recursive: true });
10721
- await writeFile5(fullPath, content, "utf-8");
11008
+ const fullPath = join15(projectRoot, relativePath);
11009
+ await mkdir6(dirname8(fullPath), { recursive: true });
11010
+ await writeFile6(fullPath, content, "utf-8");
10722
11011
  written.push(relativePath);
10723
11012
  }
10724
11013
  return written;
@@ -10854,8 +11143,8 @@ function createComponentsCommand() {
10854
11143
  // src/commands/import-cmd.ts
10855
11144
  import chalk26 from "chalk";
10856
11145
  import ora6 from "ora";
10857
- import { writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
10858
- import { resolve as resolve15, join as join15, dirname as dirname9 } from "path";
11146
+ import { writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
11147
+ import { resolve as resolve15, join as join16, dirname as dirname9 } from "path";
10859
11148
  import { existsSync as existsSync24 } from "fs";
10860
11149
  import {
10861
11150
  FigmaClient,
@@ -11003,9 +11292,9 @@ async function importFigmaAction(urlOrKey, opts) {
11003
11292
  stats.filesWritten.push(filePath);
11004
11293
  return;
11005
11294
  }
11006
- const fullPath = join15(projectRoot, filePath);
11007
- await mkdir6(dirname9(fullPath), { recursive: true });
11008
- await writeFile6(fullPath, content, "utf-8");
11295
+ const fullPath = join16(projectRoot, filePath);
11296
+ await mkdir7(dirname9(fullPath), { recursive: true });
11297
+ await writeFile7(fullPath, content, "utf-8");
11009
11298
  stats.filesWritten.push(filePath);
11010
11299
  };
11011
11300
  try {
@@ -11079,7 +11368,7 @@ async function importFigmaAction(urlOrKey, opts) {
11079
11368
  });
11080
11369
  if (dryRun) stats.filesWritten.push(FIGMA_COMPONENT_MAP_FILENAME);
11081
11370
  else
11082
- await writeFile6(
11371
+ await writeFile7(
11083
11372
  resolve15(projectRoot, FIGMA_COMPONENT_MAP_FILENAME),
11084
11373
  JSON.stringify(componentMapObj, null, 2),
11085
11374
  "utf-8"
@@ -11119,7 +11408,7 @@ async function importFigmaAction(urlOrKey, opts) {
11119
11408
  });
11120
11409
  await dsm.save();
11121
11410
  } else {
11122
- await writeFile6(
11411
+ await writeFile7(
11123
11412
  configPath,
11124
11413
  `/**
11125
11414
  * Design System Configuration
@@ -11134,10 +11423,10 @@ export const config = ${JSON.stringify(fullConfig, null, 2)} as const
11134
11423
  stats.configUpdated = true;
11135
11424
  spinner.succeed("design-system.config.ts updated");
11136
11425
  spinner.start("Ensuring root layout...");
11137
- const layoutPath = join15(projectRoot, "app/layout.tsx");
11426
+ const layoutPath = join16(projectRoot, "app/layout.tsx");
11138
11427
  if (!existsSync24(layoutPath)) {
11139
- await mkdir6(dirname9(layoutPath), { recursive: true });
11140
- await writeFile6(layoutPath, MINIMAL_ROOT_LAYOUT, "utf-8");
11428
+ await mkdir7(dirname9(layoutPath), { recursive: true });
11429
+ await writeFile7(layoutPath, MINIMAL_ROOT_LAYOUT, "utf-8");
11141
11430
  stats.filesWritten.push("app/layout.tsx");
11142
11431
  }
11143
11432
  spinner.succeed("Root layout OK");
@@ -11226,7 +11515,7 @@ async function dsRegenerateCommand() {
11226
11515
  import chalk28 from "chalk";
11227
11516
  import ora8 from "ora";
11228
11517
  import { readFileSync as readFileSync16, existsSync as existsSync25 } from "fs";
11229
- import { join as join16 } from "path";
11518
+ import { join as join17 } from "path";
11230
11519
  import { DesignSystemManager as DesignSystemManager15, CLI_VERSION as CLI_VERSION4 } from "@getcoherent/core";
11231
11520
 
11232
11521
  // src/utils/migrations.ts
@@ -11395,7 +11684,7 @@ var EXPECTED_CSS_VARS = [
11395
11684
  "--sidebar-ring"
11396
11685
  ];
11397
11686
  function checkMissingCssVars(projectRoot) {
11398
- const globalsPath = join16(projectRoot, "app", "globals.css");
11687
+ const globalsPath = join17(projectRoot, "app", "globals.css");
11399
11688
  if (!existsSync25(globalsPath)) return [];
11400
11689
  try {
11401
11690
  const content = readFileSync16(globalsPath, "utf-8");
@@ -11405,7 +11694,7 @@ function checkMissingCssVars(projectRoot) {
11405
11694
  }
11406
11695
  }
11407
11696
  function patchGlobalsCss(projectRoot, missingVars) {
11408
- const globalsPath = join16(projectRoot, "app", "globals.css");
11697
+ const globalsPath = join17(projectRoot, "app", "globals.css");
11409
11698
  if (!existsSync25(globalsPath) || missingVars.length === 0) return;
11410
11699
  const { writeFileSync: writeFileSync13 } = __require("fs");
11411
11700
  let content = readFileSync16(globalsPath, "utf-8");
@@ -11487,14 +11776,14 @@ async function undoCommand(options) {
11487
11776
  import chalk30 from "chalk";
11488
11777
  import ora9 from "ora";
11489
11778
  import { existsSync as existsSync26, readFileSync as readFileSync17 } from "fs";
11490
- import { join as join17, relative as relative4, dirname as dirname10 } from "path";
11779
+ import { join as join18, relative as relative5, dirname as dirname10 } from "path";
11491
11780
  import { readdir as readdir4, readFile as readFile7 } from "fs/promises";
11492
11781
  import { DesignSystemManager as DesignSystemManager16 } from "@getcoherent/core";
11493
11782
  import { loadManifest as loadManifest12, saveManifest as saveManifest5, findSharedComponent } from "@getcoherent/core";
11494
11783
  function extractTokensFromProject(projectRoot) {
11495
11784
  const lightColors = {};
11496
11785
  const darkColors = {};
11497
- const globalsPath = join17(projectRoot, "app", "globals.css");
11786
+ const globalsPath = join18(projectRoot, "app", "globals.css");
11498
11787
  if (existsSync26(globalsPath)) {
11499
11788
  const css = readFileSync17(globalsPath, "utf-8");
11500
11789
  const rootMatch = css.match(/:root\s*\{([^}]+)\}/s);
@@ -11502,7 +11791,7 @@ function extractTokensFromProject(projectRoot) {
11502
11791
  const darkMatch = css.match(/\.dark\s*\{([^}]+)\}/s);
11503
11792
  if (darkMatch) parseVarsInto(darkMatch[1], darkColors);
11504
11793
  }
11505
- const layoutPath = join17(projectRoot, "app", "layout.tsx");
11794
+ const layoutPath = join18(projectRoot, "app", "layout.tsx");
11506
11795
  let layoutCode = "";
11507
11796
  if (existsSync26(layoutPath)) {
11508
11797
  layoutCode = readFileSync17(layoutPath, "utf-8");
@@ -11546,14 +11835,14 @@ function parseVarsInto(block, target) {
11546
11835
  }
11547
11836
  async function detectCustomComponents(projectRoot, allPageCode) {
11548
11837
  const results = [];
11549
- const componentsDir = join17(projectRoot, "components");
11838
+ const componentsDir = join18(projectRoot, "components");
11550
11839
  if (!existsSync26(componentsDir)) return results;
11551
11840
  const files = [];
11552
11841
  await walkForTsx(componentsDir, files, ["ui"]);
11553
11842
  const fileResults = await Promise.all(
11554
11843
  files.map(async (filePath) => {
11555
11844
  const code = await readFile7(filePath, "utf-8");
11556
- const relFile = relative4(projectRoot, filePath);
11845
+ const relFile = relative5(projectRoot, filePath);
11557
11846
  const exportedNames = extractExportedComponentNames2(code);
11558
11847
  return exportedNames.map((name) => ({
11559
11848
  name,
@@ -11574,7 +11863,7 @@ async function walkForTsx(dir, files, skipDirs) {
11574
11863
  return;
11575
11864
  }
11576
11865
  for (const e of entries) {
11577
- const full = join17(dir, e.name);
11866
+ const full = join18(dir, e.name);
11578
11867
  if (e.isDirectory()) {
11579
11868
  if (skipDirs.includes(e.name) || e.name.startsWith(".")) continue;
11580
11869
  await walkForTsx(full, files, skipDirs);
@@ -11648,14 +11937,14 @@ async function discoverPages(appDir) {
11648
11937
  return;
11649
11938
  }
11650
11939
  for (const entry of entries) {
11651
- const full = join17(dir, entry.name);
11940
+ const full = join18(dir, entry.name);
11652
11941
  if (entry.isDirectory()) {
11653
11942
  if (["design-system", "api", "_not-found"].includes(entry.name)) continue;
11654
11943
  if (entry.name.startsWith(".")) continue;
11655
11944
  await walk(full);
11656
11945
  } else if (entry.name === "page.tsx" || entry.name === "page.jsx") {
11657
11946
  const code = await readFile7(full, "utf-8");
11658
- const routeDir = dirname10(relative4(appDir, full));
11947
+ const routeDir = dirname10(relative5(appDir, full));
11659
11948
  let route = routeDir === "." ? "/" : "/" + routeDir;
11660
11949
  route = route.replace(/\/\([^)]+\)/g, "");
11661
11950
  if (!route.startsWith("/")) route = "/" + route;
@@ -11725,7 +12014,7 @@ async function syncCommand(options = {}) {
11725
12014
  if (dryRun) console.log(chalk30.yellow(" [dry-run] No files will be written\n"));
11726
12015
  const spinner = ora9("Scanning project files...").start();
11727
12016
  try {
11728
- const appDir = join17(project.root, "app");
12017
+ const appDir = join18(project.root, "app");
11729
12018
  if (!existsSync26(appDir)) {
11730
12019
  spinner.fail("No app/ directory found");
11731
12020
  process.exit(1);
@@ -11952,14 +12241,14 @@ async function syncCommand(options = {}) {
11952
12241
 
11953
12242
  // src/utils/update-notifier.ts
11954
12243
  import { existsSync as existsSync27, mkdirSync as mkdirSync8, readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
11955
- import { join as join18 } from "path";
12244
+ import { join as join19 } from "path";
11956
12245
  import { homedir } from "os";
11957
12246
  import chalk31 from "chalk";
11958
12247
  import { CLI_VERSION as CLI_VERSION5 } from "@getcoherent/core";
11959
12248
  var DEBUG5 = process.env.COHERENT_DEBUG === "1";
11960
12249
  var PACKAGE_NAME = "@getcoherent/cli";
11961
- var CACHE_DIR = join18(homedir(), ".coherent");
11962
- var CACHE_FILE = join18(CACHE_DIR, "update-check.json");
12250
+ var CACHE_DIR = join19(homedir(), ".coherent");
12251
+ var CACHE_FILE = join19(CACHE_DIR, "update-check.json");
11963
12252
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11964
12253
  function readCache() {
11965
12254
  try {