@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.
- package/dist/index.js +2048 -1759
- 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:
|
|
4716
|
-
await
|
|
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/
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
var
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
if (
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
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
|
|
5086
|
+
return inSingle || inDouble || inTemplate;
|
|
5081
5087
|
}
|
|
5082
|
-
|
|
5083
|
-
const
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
const
|
|
5088
|
-
if (
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
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
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
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
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
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
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
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
|
|
5122
|
+
return issues;
|
|
5195
5123
|
}
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
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
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
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
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
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
|
-
|
|
5331
|
-
(
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
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
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
if (
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
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 (
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
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
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
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
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
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
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
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
|
|
5444
|
+
return issues;
|
|
5619
5445
|
}
|
|
5620
|
-
function
|
|
5621
|
-
const
|
|
5622
|
-
|
|
5623
|
-
const
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
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
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5454
|
+
const beforeEntityFix = fixed;
|
|
5455
|
+
fixed = fixed.replace(/<=/g, "<=");
|
|
5456
|
+
fixed = fixed.replace(/>=/g, ">=");
|
|
5457
|
+
fixed = fixed.replace(/&&/g, "&&");
|
|
5458
|
+
fixed = fixed.replace(/([\w)\]])\s*<\s*([\w(])/g, "$1 < $2");
|
|
5459
|
+
fixed = fixed.replace(/([\w)\]])\s*>\s*([\w(])/g, "$1 > $2");
|
|
5460
|
+
if (fixed !== beforeEntityFix) {
|
|
5461
|
+
fixes.push("Fixed syntax issues");
|
|
5633
5462
|
}
|
|
5634
|
-
|
|
5635
|
-
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
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<$2");
|
|
5465
|
+
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$2");
|
|
5466
|
+
if (fixed !== beforeLtFix) {
|
|
5467
|
+
fixes.push("escaped < in JSX text content");
|
|
5656
5468
|
}
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
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 (
|
|
5665
|
-
|
|
5666
|
-
|
|
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 (
|
|
5671
|
-
|
|
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
|
-
|
|
5674
|
-
|
|
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
|
-
|
|
5678
|
-
|
|
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
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
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
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
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 (
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
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
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
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
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
const
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
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
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
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
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
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
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
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
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
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
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
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
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
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
|
-
|
|
6125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6159
|
-
|
|
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
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
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
|
-
|
|
6188
|
-
|
|
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
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
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
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
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
|
-
|
|
6216
|
-
|
|
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
|
-
|
|
6228
|
-
|
|
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
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
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
|
-
|
|
6247
|
-
|
|
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
|
-
|
|
6257
|
-
|
|
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
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
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
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
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
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
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
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
if (
|
|
6315
|
-
|
|
6316
|
-
}
|
|
6317
|
-
const beforeLtFix = fixed;
|
|
6318
|
-
fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1<$2");
|
|
6319
|
-
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$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
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
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 (
|
|
6328
|
-
|
|
6329
|
-
|
|
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 (
|
|
6332
|
-
|
|
6333
|
-
|
|
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
|
-
|
|
6336
|
-
|
|
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
|
-
|
|
6344
|
-
|
|
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
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
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
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
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
|
-
|
|
6390
|
-
});
|
|
6165
|
+
}
|
|
6391
6166
|
}
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
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
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
"
|
|
6411
|
-
"
|
|
6412
|
-
"
|
|
6413
|
-
"
|
|
6414
|
-
"
|
|
6415
|
-
"
|
|
6416
|
-
"
|
|
6417
|
-
"
|
|
6418
|
-
"
|
|
6419
|
-
"
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
"
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
"
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
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
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
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 (
|
|
6476
|
-
|
|
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
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6498
|
-
|
|
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
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
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 (
|
|
6514
|
-
|
|
6515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
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
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
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
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
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
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
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
|
-
|
|
6587
|
-
|
|
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
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
if (
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
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
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
|
|
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
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
}
|
|
6627
|
-
|
|
6628
|
-
const
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
const
|
|
6637
|
-
|
|
6638
|
-
}
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
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
|
-
|
|
6649
|
-
}
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
const
|
|
6653
|
-
const
|
|
6654
|
-
const
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
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 (
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
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
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
6932
|
+
await mkdir3(dirname5(filePath), { recursive: true });
|
|
6752
6933
|
await writeFile(filePath, code);
|
|
6753
6934
|
}
|
|
6754
|
-
async function
|
|
6755
|
-
const
|
|
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
|
|
6759
|
-
const
|
|
6760
|
-
|
|
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
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
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
|
|
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
|
|
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 >=
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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])
|
|
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])
|
|
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(
|
|
8371
|
-
|
|
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
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
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
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
}
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
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
|
-
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
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
|
-
}
|
|
8431
|
-
console.log(chalk13.
|
|
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
|
|
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
|
|
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
|
|
8757
|
-
import { join as
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
9292
|
+
entries = readdirSync3(d, { withFileTypes: true });
|
|
9004
9293
|
} catch {
|
|
9005
9294
|
return;
|
|
9006
9295
|
}
|
|
9007
9296
|
for (const e of entries) {
|
|
9008
|
-
const full =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
9177
|
-
const compGlob =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
9284
|
-
const uiDir =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
9646
|
+
filePath = join12(projectRoot, "app", "page.tsx");
|
|
9358
9647
|
} else if (isAuth) {
|
|
9359
|
-
filePath =
|
|
9648
|
+
filePath = join12(projectRoot, "app", "(auth)", route.slice(1), "page.tsx");
|
|
9360
9649
|
} else {
|
|
9361
|
-
filePath =
|
|
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 =
|
|
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 =
|
|
9689
|
+
const uiDir = join12(projectRoot, "components", "ui");
|
|
9401
9690
|
const { mkdirSync: mkdirSync9 } = await import("fs");
|
|
9402
9691
|
mkdirSync9(uiDir, { recursive: true });
|
|
9403
|
-
writeFileSync10(
|
|
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
|
|
9618
|
-
import { resolve as resolve11, join as
|
|
9619
|
-
import { readdir as readdir3, readFile as readFile6, writeFile as
|
|
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
|
|
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 =
|
|
9641
|
-
const destPath =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
10024
|
+
const dir = join13(outRoot, "components", sub);
|
|
9736
10025
|
if (!existsSync20(dir)) continue;
|
|
9737
10026
|
try {
|
|
9738
|
-
n +=
|
|
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 =
|
|
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 =
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
10100
|
+
await writeFile5(layoutPath, layout, "utf-8");
|
|
9812
10101
|
}
|
|
9813
|
-
const sharedHeaderPath =
|
|
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
|
|
10108
|
+
await writeFile5(sharedHeaderPath, header, "utf-8");
|
|
9820
10109
|
}
|
|
9821
|
-
const guardPath =
|
|
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
|
|
10124
|
+
await writeFile5(layoutPath, layout, "utf-8");
|
|
9836
10125
|
}
|
|
9837
10126
|
} else {
|
|
9838
|
-
await
|
|
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 =
|
|
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 =
|
|
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
|
|
10058
|
-
import { resolve as resolve12, join as
|
|
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 =
|
|
10371
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
10083
10372
|
for (const e of entries) {
|
|
10084
|
-
const full =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
10705
|
-
import { join as
|
|
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 =
|
|
10720
|
-
await
|
|
10721
|
-
await
|
|
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
|
|
10858
|
-
import { resolve as resolve15, join as
|
|
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 =
|
|
11007
|
-
await
|
|
11008
|
-
await
|
|
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
|
|
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
|
|
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 =
|
|
11426
|
+
const layoutPath = join16(projectRoot, "app/layout.tsx");
|
|
11138
11427
|
if (!existsSync24(layoutPath)) {
|
|
11139
|
-
await
|
|
11140
|
-
await
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
11962
|
-
var CACHE_FILE =
|
|
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 {
|