@getcoherent/cli 0.5.3 → 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 +1753 -1653
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4517,6 +4517,7 @@ pageCode rules (shadcn/ui blocks quality):
|
|
|
4517
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.
|
|
4518
4518
|
- Hover/focus on every interactive element (hover:bg-muted, focus-visible:ring-2 focus-visible:ring-ring).
|
|
4519
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.
|
|
4520
4521
|
|
|
4521
4522
|
NEXT.JS APP ROUTER RULE (CRITICAL \u2014 invalid code fails to compile):
|
|
4522
4523
|
- "use client" and export const metadata are FORBIDDEN in the same file.
|
|
@@ -5042,1716 +5043,1785 @@ function fixGlobalsCss(projectRoot, config2) {
|
|
|
5042
5043
|
writeFileSync7(layoutPath, layoutContent, "utf-8");
|
|
5043
5044
|
}
|
|
5044
5045
|
|
|
5045
|
-
// src/
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
var
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
if (
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
return pages.filter((page, idx) => {
|
|
5085
|
-
const norm = normalize(page.route);
|
|
5086
|
-
if (seen.has(norm)) return false;
|
|
5087
|
-
seen.set(norm, idx);
|
|
5088
|
-
return true;
|
|
5089
|
-
});
|
|
5090
|
-
}
|
|
5091
|
-
function extractComponentIdsFromCode(code) {
|
|
5092
|
-
const ids = /* @__PURE__ */ new Set();
|
|
5093
|
-
const allMatches = code.matchAll(/@\/components\/((?:ui\/)?[a-z0-9-]+)/g);
|
|
5094
|
-
for (const m of allMatches) {
|
|
5095
|
-
if (!m[1]) continue;
|
|
5096
|
-
let id = m[1];
|
|
5097
|
-
if (id.startsWith("ui/")) id = id.slice(3);
|
|
5098
|
-
if (id === "shared" || id.startsWith("shared/")) continue;
|
|
5099
|
-
if (id) ids.add(id);
|
|
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;
|
|
5100
5085
|
}
|
|
5101
|
-
return
|
|
5086
|
+
return inSingle || inDouble || inTemplate;
|
|
5102
5087
|
}
|
|
5103
|
-
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
const
|
|
5109
|
-
if (
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
)
|
|
5117
|
-
);
|
|
5118
|
-
continue;
|
|
5119
|
-
}
|
|
5120
|
-
try {
|
|
5121
|
-
const fullPath = resolve5(projectRoot, e.file);
|
|
5122
|
-
const sharedSnippet = (await readFile(fullPath)).slice(0, 600);
|
|
5123
|
-
const sharedTokens = new Set(sharedSnippet.match(/\b[a-zA-Z0-9-]{4,}\b/g) ?? []);
|
|
5124
|
-
const pageTokens = pageCode.match(/\b[a-zA-Z0-9-]+\b/g) ?? [];
|
|
5125
|
-
let overlap = 0;
|
|
5126
|
-
for (const t of sharedTokens) {
|
|
5127
|
-
if (pageTokens.includes(t)) overlap++;
|
|
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;
|
|
5128
5101
|
}
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
\u26A0 Page "${pageName}" contains inline code similar to ${e.id} (${e.name}). Consider using the shared component instead.`
|
|
5134
|
-
)
|
|
5135
|
-
);
|
|
5102
|
+
const blockStart = line.indexOf("/*");
|
|
5103
|
+
if (blockStart !== -1 && !line.includes("*/")) {
|
|
5104
|
+
inBlockComment = true;
|
|
5105
|
+
continue;
|
|
5136
5106
|
}
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
`Design system config not found at ${configPath}
|
|
5145
|
-
Run "coherent init" first to create a project.`
|
|
5146
|
-
);
|
|
5147
|
-
}
|
|
5148
|
-
const manager = new DesignSystemManager3(configPath);
|
|
5149
|
-
await manager.load();
|
|
5150
|
-
return manager.getConfig();
|
|
5151
|
-
}
|
|
5152
|
-
function requireProject() {
|
|
5153
|
-
const project = findConfig();
|
|
5154
|
-
if (!project) {
|
|
5155
|
-
exitNotCoherent();
|
|
5156
|
-
}
|
|
5157
|
-
warnIfVolatile(project.root);
|
|
5158
|
-
return project;
|
|
5159
|
-
}
|
|
5160
|
-
async function resolveTargetFlags(message, options, config2, projectRoot) {
|
|
5161
|
-
if (options.component) {
|
|
5162
|
-
const manifest = await loadManifest4(projectRoot);
|
|
5163
|
-
const target = options.component;
|
|
5164
|
-
const entry = manifest.shared.find(
|
|
5165
|
-
(s) => s.name.toLowerCase() === target.toLowerCase() || s.id.toLowerCase() === target.toLowerCase()
|
|
5166
|
-
);
|
|
5167
|
-
if (entry) {
|
|
5168
|
-
const filePath = resolve5(projectRoot, entry.file);
|
|
5169
|
-
let currentCode = "";
|
|
5170
|
-
if (existsSync13(filePath)) {
|
|
5171
|
-
currentCode = readFileSync8(filePath, "utf-8");
|
|
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
|
+
}
|
|
5172
5114
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
${currentCode}
|
|
5178
|
-
\`\`\`` : "";
|
|
5179
|
-
return `Modify the shared component ${entry.name} (${entry.id}, file: ${entry.file}): ${message}. Read the current code below and apply the requested changes. Return the full updated component code as pageCode.${codeSnippet}`;
|
|
5180
|
-
}
|
|
5181
|
-
console.log(chalk8.yellow(`
|
|
5182
|
-
\u26A0\uFE0F Component "${target}" not found in shared components.`));
|
|
5183
|
-
console.log(chalk8.dim(" Available: " + manifest.shared.map((s) => `${s.id} ${s.name}`).join(", ")));
|
|
5184
|
-
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
5185
|
-
}
|
|
5186
|
-
if (options.page) {
|
|
5187
|
-
const target = options.page;
|
|
5188
|
-
const page = config2.pages.find(
|
|
5189
|
-
(p) => p.name.toLowerCase() === target.toLowerCase() || p.id.toLowerCase() === target.toLowerCase() || p.route === target || p.route === "/" + target
|
|
5190
|
-
);
|
|
5191
|
-
if (page) {
|
|
5192
|
-
const relPath = page.route === "/" ? "app/page.tsx" : `app${page.route}/page.tsx`;
|
|
5193
|
-
const filePath = resolve5(projectRoot, relPath);
|
|
5194
|
-
let currentCode = "";
|
|
5195
|
-
if (existsSync13(filePath)) {
|
|
5196
|
-
currentCode = readFileSync8(filePath, "utf-8");
|
|
5115
|
+
} else {
|
|
5116
|
+
pattern.lastIndex = 0;
|
|
5117
|
+
if (pattern.test(line)) {
|
|
5118
|
+
issues.push({ line: i + 1, type, message, severity });
|
|
5197
5119
|
}
|
|
5198
|
-
const codeSnippet = currentCode ? `
|
|
5199
|
-
|
|
5200
|
-
Current code of ${page.name} page:
|
|
5201
|
-
\`\`\`tsx
|
|
5202
|
-
${currentCode}
|
|
5203
|
-
\`\`\`` : "";
|
|
5204
|
-
return `Update page "${page.name}" (id: ${page.id}, route: ${page.route}, file: ${relPath}): ${message}. Read the current code below and apply the requested changes.${codeSnippet}`;
|
|
5205
5120
|
}
|
|
5206
|
-
console.log(chalk8.yellow(`
|
|
5207
|
-
\u26A0\uFE0F Page "${target}" not found.`));
|
|
5208
|
-
console.log(chalk8.dim(" Available: " + config2.pages.map((p) => `${p.id} (${p.route})`).join(", ")));
|
|
5209
|
-
console.log(chalk8.dim(" Proceeding with message as-is...\n"));
|
|
5210
|
-
}
|
|
5211
|
-
if (options.token) {
|
|
5212
|
-
const target = options.token;
|
|
5213
|
-
return `Change design token "${target}": ${message}. Update the token value in design-system.config.ts and ensure globals.css reflects the change.`;
|
|
5214
5121
|
}
|
|
5215
|
-
return
|
|
5122
|
+
return issues;
|
|
5216
5123
|
}
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
};
|
|
5227
|
-
var PAGE_RELATIONSHIP_RULES = [
|
|
5228
|
-
{
|
|
5229
|
-
trigger: /\/(products|catalog|marketplace|listings|shop|store)\b/i,
|
|
5230
|
-
related: [{ id: "product-detail", name: "Product Detail", route: "/products/[id]" }]
|
|
5231
|
-
},
|
|
5232
|
-
{
|
|
5233
|
-
trigger: /\/(blog|news|articles|posts)\b/i,
|
|
5234
|
-
related: [{ id: "article-detail", name: "Article", route: "/blog/[slug]" }]
|
|
5235
|
-
},
|
|
5236
|
-
{
|
|
5237
|
-
trigger: /\/(campaigns|ads|ad-campaigns)\b/i,
|
|
5238
|
-
related: [{ id: "campaign-detail", name: "Campaign Detail", route: "/campaigns/[id]" }]
|
|
5239
|
-
},
|
|
5240
|
-
{
|
|
5241
|
-
trigger: /\/(dashboard|admin)\b/i,
|
|
5242
|
-
related: [{ id: "settings", name: "Settings", route: "/settings" }]
|
|
5243
|
-
},
|
|
5244
|
-
{
|
|
5245
|
-
trigger: /\/pricing\b/i,
|
|
5246
|
-
related: [{ id: "checkout", name: "Checkout", route: "/checkout" }]
|
|
5247
|
-
}
|
|
5248
|
-
];
|
|
5249
|
-
function extractInternalLinks(code) {
|
|
5250
|
-
const links = /* @__PURE__ */ new Set();
|
|
5251
|
-
const hrefRe = /href\s*=\s*["'](\/[a-z0-9/-]*)["']/gi;
|
|
5252
|
-
let m;
|
|
5253
|
-
while ((m = hrefRe.exec(code)) !== null) {
|
|
5254
|
-
const route = m[1];
|
|
5255
|
-
if (route === "/" || route.startsWith("/design-system") || route.startsWith("/#") || route.startsWith("/api"))
|
|
5256
|
-
continue;
|
|
5257
|
-
links.add(route);
|
|
5258
|
-
}
|
|
5259
|
-
return [...links];
|
|
5260
|
-
}
|
|
5261
|
-
function inferRelatedPages(plannedPages) {
|
|
5262
|
-
const plannedRoutes = new Set(plannedPages.map((p) => p.route));
|
|
5263
|
-
const inferred = [];
|
|
5264
|
-
for (const { route } of plannedPages) {
|
|
5265
|
-
const authRelated = AUTH_FLOW_PATTERNS[route];
|
|
5266
|
-
if (authRelated) {
|
|
5267
|
-
for (const rel of authRelated) {
|
|
5268
|
-
if (!plannedRoutes.has(rel)) {
|
|
5269
|
-
const slug = rel.slice(1);
|
|
5270
|
-
const name = slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
5271
|
-
inferred.push({ id: slug, name, route: rel });
|
|
5272
|
-
plannedRoutes.add(rel);
|
|
5273
|
-
}
|
|
5274
|
-
}
|
|
5275
|
-
}
|
|
5276
|
-
for (const rule of PAGE_RELATIONSHIP_RULES) {
|
|
5277
|
-
if (rule.trigger.test(route)) {
|
|
5278
|
-
for (const rel of rule.related) {
|
|
5279
|
-
if (!plannedRoutes.has(rel.route)) {
|
|
5280
|
-
inferred.push(rel);
|
|
5281
|
-
plannedRoutes.add(rel.route);
|
|
5282
|
-
}
|
|
5283
|
-
}
|
|
5284
|
-
}
|
|
5285
|
-
}
|
|
5286
|
-
}
|
|
5287
|
-
return inferred;
|
|
5288
|
-
}
|
|
5289
|
-
function impliesFullWebsite(message) {
|
|
5290
|
-
return /\b(create|build|make|design)\b.{0,80}\b(website|web\s*site|web\s*app|application|app|platform|portal|marketplace|site)\b/i.test(
|
|
5291
|
-
message
|
|
5292
|
-
);
|
|
5293
|
-
}
|
|
5294
|
-
function extractPageNamesFromMessage(message) {
|
|
5295
|
-
const pages = [];
|
|
5296
|
-
const known = {
|
|
5297
|
-
home: "/",
|
|
5298
|
-
landing: "/",
|
|
5299
|
-
dashboard: "/dashboard",
|
|
5300
|
-
about: "/about",
|
|
5301
|
-
"about us": "/about",
|
|
5302
|
-
contact: "/contact",
|
|
5303
|
-
contacts: "/contacts",
|
|
5304
|
-
pricing: "/pricing",
|
|
5305
|
-
settings: "/settings",
|
|
5306
|
-
account: "/account",
|
|
5307
|
-
"personal account": "/account",
|
|
5308
|
-
registration: "/registration",
|
|
5309
|
-
signup: "/signup",
|
|
5310
|
-
"sign up": "/signup",
|
|
5311
|
-
login: "/login",
|
|
5312
|
-
"sign in": "/login",
|
|
5313
|
-
catalogue: "/catalogue",
|
|
5314
|
-
catalog: "/catalog",
|
|
5315
|
-
blog: "/blog",
|
|
5316
|
-
portfolio: "/portfolio",
|
|
5317
|
-
features: "/features",
|
|
5318
|
-
services: "/services",
|
|
5319
|
-
faq: "/faq",
|
|
5320
|
-
team: "/team"
|
|
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;
|
|
5321
5133
|
};
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
}
|
|
5331
|
-
}
|
|
5332
|
-
return pages;
|
|
5333
|
-
}
|
|
5334
|
-
function normalizeRequest(request, config2) {
|
|
5335
|
-
const changes = request.changes;
|
|
5336
|
-
const VALID_TYPES = [
|
|
5337
|
-
"update-token",
|
|
5338
|
-
"add-component",
|
|
5339
|
-
"modify-component",
|
|
5340
|
-
"add-layout-block",
|
|
5341
|
-
"modify-layout-block",
|
|
5342
|
-
"add-page",
|
|
5343
|
-
"update-page",
|
|
5344
|
-
"update-navigation",
|
|
5345
|
-
"link-shared",
|
|
5346
|
-
"promote-and-link"
|
|
5347
|
-
];
|
|
5348
|
-
if (!VALID_TYPES.includes(request.type)) {
|
|
5349
|
-
return { error: `Unknown action "${request.type}". Valid: ${VALID_TYPES.join(", ")}` };
|
|
5350
|
-
}
|
|
5351
|
-
const findPage = (target) => config2.pages.find(
|
|
5352
|
-
(p) => p.id === target || p.route === target || p.name?.toLowerCase() === String(target).toLowerCase()
|
|
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))
|
|
5353
5142
|
);
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
};
|
|
5464
|
-
}
|
|
5465
|
-
if (sourcePage.id !== request.target) {
|
|
5466
|
-
return { ...request, target: sourcePage.id };
|
|
5467
|
-
}
|
|
5468
|
-
break;
|
|
5469
|
-
}
|
|
5470
|
-
}
|
|
5471
|
-
return request;
|
|
5472
|
-
}
|
|
5473
|
-
function applyDefaults(request) {
|
|
5474
|
-
if (request.type === "add-page" && request.changes && typeof request.changes === "object") {
|
|
5475
|
-
const changes = request.changes;
|
|
5476
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5477
|
-
const name = changes.name || "New Page";
|
|
5478
|
-
let id = changes.id || name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
5479
|
-
if (!/^[a-z]/.test(id)) id = `page-${id}`;
|
|
5480
|
-
const route = changes.route || `/${id}`;
|
|
5481
|
-
const hasPageCode = typeof changes.pageCode === "string" && changes.pageCode.trim() !== "";
|
|
5482
|
-
const base = {
|
|
5483
|
-
id,
|
|
5484
|
-
name,
|
|
5485
|
-
route: route.startsWith("/") ? route : `/${route}`,
|
|
5486
|
-
layout: changes.layout || "centered",
|
|
5487
|
-
title: changes.title || name,
|
|
5488
|
-
description: changes.description || `${name} page`,
|
|
5489
|
-
createdAt: changes.createdAt || now,
|
|
5490
|
-
updatedAt: changes.updatedAt || now,
|
|
5491
|
-
requiresAuth: changes.requiresAuth ?? false,
|
|
5492
|
-
noIndex: changes.noIndex ?? false
|
|
5493
|
-
};
|
|
5494
|
-
const sections = Array.isArray(changes.sections) ? changes.sections.map((section, idx) => ({
|
|
5495
|
-
id: section.id || `section-${idx}`,
|
|
5496
|
-
name: section.name || `Section ${idx + 1}`,
|
|
5497
|
-
componentId: section.componentId || "button",
|
|
5498
|
-
order: typeof section.order === "number" ? section.order : idx,
|
|
5499
|
-
props: section.props || {}
|
|
5500
|
-
})) : [];
|
|
5501
|
-
return {
|
|
5502
|
-
...request,
|
|
5503
|
-
changes: {
|
|
5504
|
-
...base,
|
|
5505
|
-
sections,
|
|
5506
|
-
...hasPageCode ? { pageCode: changes.pageCode, generatedWithPageCode: true } : {},
|
|
5507
|
-
...changes.pageType ? { pageType: changes.pageType } : {},
|
|
5508
|
-
...changes.structuredContent ? { structuredContent: changes.structuredContent } : {}
|
|
5509
|
-
}
|
|
5510
|
-
};
|
|
5511
|
-
}
|
|
5512
|
-
if (request.type === "add-component" && request.changes && typeof request.changes === "object") {
|
|
5513
|
-
const changes = request.changes;
|
|
5514
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5515
|
-
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
5516
|
-
let normalizedVariants = [];
|
|
5517
|
-
if (Array.isArray(changes.variants)) {
|
|
5518
|
-
normalizedVariants = changes.variants.map((v) => {
|
|
5519
|
-
if (typeof v === "string") return { name: v, className: "" };
|
|
5520
|
-
if (v && typeof v === "object" && "name" in v) {
|
|
5521
|
-
return {
|
|
5522
|
-
name: v.name,
|
|
5523
|
-
className: v.className ?? ""
|
|
5524
|
-
};
|
|
5525
|
-
}
|
|
5526
|
-
return { name: "default", className: "" };
|
|
5527
|
-
});
|
|
5528
|
-
}
|
|
5529
|
-
let normalizedSizes = [];
|
|
5530
|
-
if (Array.isArray(changes.sizes)) {
|
|
5531
|
-
normalizedSizes = changes.sizes.map((s) => {
|
|
5532
|
-
if (typeof s === "string") {
|
|
5533
|
-
const name = validSizeNames.includes(s) ? s : "md";
|
|
5534
|
-
return { name, className: "" };
|
|
5535
|
-
}
|
|
5536
|
-
if (s && typeof s === "object" && "name" in s) {
|
|
5537
|
-
const raw = s.name;
|
|
5538
|
-
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
5539
|
-
return { name, className: s.className ?? "" };
|
|
5540
|
-
}
|
|
5541
|
-
return { name: "md", className: "" };
|
|
5542
|
-
});
|
|
5543
|
-
}
|
|
5544
|
-
return {
|
|
5545
|
-
...request,
|
|
5546
|
-
changes: {
|
|
5547
|
-
...changes,
|
|
5548
|
-
variants: normalizedVariants,
|
|
5549
|
-
sizes: normalizedSizes,
|
|
5550
|
-
createdAt: now,
|
|
5551
|
-
updatedAt: now
|
|
5552
|
-
}
|
|
5553
|
-
};
|
|
5554
|
-
}
|
|
5555
|
-
if (request.type === "modify-component" && request.changes && typeof request.changes === "object") {
|
|
5556
|
-
const changes = request.changes;
|
|
5557
|
-
const validSizeNames = ["xs", "sm", "md", "lg", "xl"];
|
|
5558
|
-
let normalizedVariants;
|
|
5559
|
-
if (Array.isArray(changes.variants)) {
|
|
5560
|
-
normalizedVariants = changes.variants.map((v) => {
|
|
5561
|
-
if (typeof v === "string") return { name: v, className: "" };
|
|
5562
|
-
if (v && typeof v === "object" && "name" in v) {
|
|
5563
|
-
return {
|
|
5564
|
-
name: v.name,
|
|
5565
|
-
className: v.className ?? ""
|
|
5566
|
-
};
|
|
5567
|
-
}
|
|
5568
|
-
return { name: "default", className: "" };
|
|
5569
|
-
});
|
|
5570
|
-
}
|
|
5571
|
-
let normalizedSizes;
|
|
5572
|
-
if (Array.isArray(changes.sizes)) {
|
|
5573
|
-
normalizedSizes = changes.sizes.map((s) => {
|
|
5574
|
-
if (typeof s === "string") {
|
|
5575
|
-
const name = validSizeNames.includes(s) ? s : "md";
|
|
5576
|
-
return { name, className: "" };
|
|
5577
|
-
}
|
|
5578
|
-
if (s && typeof s === "object" && "name" in s) {
|
|
5579
|
-
const raw = s.name;
|
|
5580
|
-
const name = validSizeNames.includes(raw) ? raw : "md";
|
|
5581
|
-
return { name, className: s.className ?? "" };
|
|
5582
|
-
}
|
|
5583
|
-
return { name: "md", className: "" };
|
|
5584
|
-
});
|
|
5585
|
-
}
|
|
5586
|
-
return {
|
|
5587
|
-
...request,
|
|
5588
|
-
changes: {
|
|
5589
|
-
...changes,
|
|
5590
|
-
...normalizedVariants !== void 0 && { variants: normalizedVariants },
|
|
5591
|
-
...normalizedSizes !== void 0 && { sizes: normalizedSizes }
|
|
5592
|
-
}
|
|
5593
|
-
};
|
|
5594
|
-
}
|
|
5595
|
-
return request;
|
|
5596
|
-
}
|
|
5597
|
-
|
|
5598
|
-
// src/utils/page-analyzer.ts
|
|
5599
|
-
var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
|
|
5600
|
-
var VISUAL_WORDS = /\b(grid lines?|glow|radial|gradient|blur|shadow|overlay|animation|particles?|dots?|vertical|horizontal|decorat|behind|background|divider|spacer|wrapper|container|inner|outer|absolute|relative|translate|opacity|z-index|transition)\b/i;
|
|
5601
|
-
function analyzePageCode(code) {
|
|
5602
|
-
return {
|
|
5603
|
-
sections: extractSections(code),
|
|
5604
|
-
componentUsage: extractComponentUsage(code),
|
|
5605
|
-
iconCount: extractIconCount(code),
|
|
5606
|
-
layoutPattern: inferLayoutPattern(code),
|
|
5607
|
-
hasForm: detectFormUsage(code),
|
|
5608
|
-
analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5609
|
-
};
|
|
5610
|
-
}
|
|
5611
|
-
function extractSections(code) {
|
|
5612
|
-
const sections = [];
|
|
5613
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5614
|
-
const commentRe = /\{\/\*\s*(.+?)\s*\*\/\}/g;
|
|
5615
|
-
let m;
|
|
5616
|
-
while ((m = commentRe.exec(code)) !== null) {
|
|
5617
|
-
const raw = m[1].trim();
|
|
5618
|
-
const name = raw.replace(/[─━—–]+/g, "").replace(/\s*section\s*$/i, "").replace(/^section\s*:\s*/i, "").trim();
|
|
5619
|
-
if (!name || name.length <= 1 || name.length >= 40) continue;
|
|
5620
|
-
if (seen.has(name.toLowerCase())) continue;
|
|
5621
|
-
const wordCount = name.split(/\s+/).length;
|
|
5622
|
-
if (wordCount > 5) continue;
|
|
5623
|
-
if (/[{}()=<>;:`"']/.test(name)) continue;
|
|
5624
|
-
if (/^[a-z]/.test(name) && wordCount > 2) continue;
|
|
5625
|
-
if (VISUAL_WORDS.test(name)) continue;
|
|
5626
|
-
seen.add(name.toLowerCase());
|
|
5627
|
-
sections.push({ name, order: sections.length });
|
|
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
|
+
);
|
|
5628
5252
|
}
|
|
5629
|
-
|
|
5630
|
-
const
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
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
|
+
});
|
|
5636
5263
|
}
|
|
5637
5264
|
}
|
|
5638
5265
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
importedComponents.push(...names);
|
|
5649
|
-
}
|
|
5650
|
-
for (const comp of importedComponents) {
|
|
5651
|
-
const re = new RegExp(`<${comp}[\\s/>]`, "g");
|
|
5652
|
-
const matches = code.match(re);
|
|
5653
|
-
usage[comp] = matches ? matches.length : 0;
|
|
5654
|
-
}
|
|
5655
|
-
return usage;
|
|
5656
|
-
}
|
|
5657
|
-
function extractIconCount(code) {
|
|
5658
|
-
const m = code.match(/import\s*\{([^}]+)\}\s*from\s*['"]lucide-react['"]/);
|
|
5659
|
-
if (!m) return 0;
|
|
5660
|
-
return m[1].split(",").map((s) => s.trim()).filter(Boolean).length;
|
|
5661
|
-
}
|
|
5662
|
-
function inferLayoutPattern(code) {
|
|
5663
|
-
const funcBodyMatch = code.match(/return\s*\(\s*(<[^]*)/s);
|
|
5664
|
-
const topLevel = funcBodyMatch ? funcBodyMatch[1].slice(0, 500) : code.slice(0, 800);
|
|
5665
|
-
if (/grid-cols|grid\s+md:grid-cols|grid\s+lg:grid-cols/.test(topLevel)) return "grid";
|
|
5666
|
-
if (/sidebar|aside/.test(topLevel)) return "sidebar";
|
|
5667
|
-
if (/max-w-\d|mx-auto|container/.test(topLevel)) return "centered";
|
|
5668
|
-
if (/min-h-screen|min-h-svh/.test(topLevel)) return "full-width";
|
|
5669
|
-
return "unknown";
|
|
5670
|
-
}
|
|
5671
|
-
function detectFormUsage(code) {
|
|
5672
|
-
const importRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/ui\/[^'"]+['"]/g;
|
|
5673
|
-
let m;
|
|
5674
|
-
while ((m = importRe.exec(code)) !== null) {
|
|
5675
|
-
const names = m[1].split(",").map((s) => s.trim());
|
|
5676
|
-
if (names.some((n) => FORM_COMPONENTS.has(n))) return true;
|
|
5677
|
-
}
|
|
5678
|
-
return false;
|
|
5679
|
-
}
|
|
5680
|
-
function summarizePageAnalysis(pageName, route, analysis) {
|
|
5681
|
-
const parts = [`${pageName} (${route})`];
|
|
5682
|
-
if (analysis.sections && analysis.sections.length > 0) {
|
|
5683
|
-
parts.push(`sections: ${analysis.sections.map((s) => s.name).join(", ")}`);
|
|
5684
|
-
}
|
|
5685
|
-
if (analysis.componentUsage) {
|
|
5686
|
-
const entries = Object.entries(analysis.componentUsage).filter(([, c]) => c > 0);
|
|
5687
|
-
if (entries.length > 0) {
|
|
5688
|
-
parts.push(`uses: ${entries.map(([n, c]) => `${n}(${c})`).join(", ")}`);
|
|
5689
|
-
}
|
|
5690
|
-
}
|
|
5691
|
-
if (analysis.layoutPattern && analysis.layoutPattern !== "unknown") {
|
|
5692
|
-
parts.push(`layout: ${analysis.layoutPattern}`);
|
|
5693
|
-
}
|
|
5694
|
-
if (analysis.hasForm) parts.push("has-form");
|
|
5695
|
-
return `- ${parts.join(". ")}`;
|
|
5696
|
-
}
|
|
5697
|
-
|
|
5698
|
-
// src/utils/concurrency.ts
|
|
5699
|
-
async function pMap(items, fn, concurrency = 3) {
|
|
5700
|
-
const results = new Array(items.length);
|
|
5701
|
-
let nextIndex = 0;
|
|
5702
|
-
async function worker() {
|
|
5703
|
-
while (nextIndex < items.length) {
|
|
5704
|
-
const i = nextIndex++;
|
|
5705
|
-
results[i] = await fn(items[i], i);
|
|
5706
|
-
}
|
|
5707
|
-
}
|
|
5708
|
-
const workers = Array.from({ length: Math.min(concurrency, items.length) }, () => worker());
|
|
5709
|
-
await Promise.all(workers);
|
|
5710
|
-
return results;
|
|
5711
|
-
}
|
|
5712
|
-
|
|
5713
|
-
// src/commands/chat/split-generator.ts
|
|
5714
|
-
function buildExistingPagesContext(config2) {
|
|
5715
|
-
const pages = config2.pages || [];
|
|
5716
|
-
const analyzed = pages.filter((p) => p.pageAnalysis);
|
|
5717
|
-
if (analyzed.length === 0) return "";
|
|
5718
|
-
const lines = analyzed.map((p) => {
|
|
5719
|
-
return summarizePageAnalysis(p.name || p.id, p.route, p.pageAnalysis);
|
|
5720
|
-
});
|
|
5721
|
-
let ctx = `EXISTING PAGES CONTEXT:
|
|
5722
|
-
${lines.join("\n")}
|
|
5723
|
-
|
|
5724
|
-
Use consistent component choices, spacing, and layout patterns across all pages. Match the style and structure of existing pages.`;
|
|
5725
|
-
const sp = config2.stylePatterns;
|
|
5726
|
-
if (sp && typeof sp === "object") {
|
|
5727
|
-
const parts = [];
|
|
5728
|
-
if (sp.card) parts.push(`Cards: ${sp.card}`);
|
|
5729
|
-
if (sp.section) parts.push(`Sections: ${sp.section}`);
|
|
5730
|
-
if (sp.terminal) parts.push(`Terminal blocks: ${sp.terminal}`);
|
|
5731
|
-
if (sp.iconContainer) parts.push(`Icon containers: ${sp.iconContainer}`);
|
|
5732
|
-
if (sp.heroHeadline) parts.push(`Hero headline: ${sp.heroHeadline}`);
|
|
5733
|
-
if (sp.sectionTitle) parts.push(`Section title: ${sp.sectionTitle}`);
|
|
5734
|
-
if (parts.length > 0) {
|
|
5735
|
-
ctx += `
|
|
5736
|
-
|
|
5737
|
-
PROJECT STYLE PATTERNS (from sync \u2014 match these exactly):
|
|
5738
|
-
${parts.join("\n")}`;
|
|
5739
|
-
}
|
|
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
|
+
});
|
|
5740
5275
|
}
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5746
|
-
|
|
5747
|
-
|
|
5748
|
-
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
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"
|
|
5755
5292
|
)
|
|
5756
5293
|
);
|
|
5757
|
-
const
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
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
|
+
});
|
|
5319
|
+
break;
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
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
|
+
});
|
|
5332
|
+
}
|
|
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
|
+
});
|
|
5340
|
+
}
|
|
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
|
+
});
|
|
5349
|
+
}
|
|
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
|
+
});
|
|
5360
|
+
}
|
|
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
|
|
5762
5364
|
);
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
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
|
+
});
|
|
5770
5372
|
}
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
if (
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
if (gridPatterns.length > 0) lines.push(`Grids: ${gridPatterns.join(", ")}`);
|
|
5780
|
-
if (lines.length === 0) return "";
|
|
5781
|
-
return `STYLE CONTEXT (match these patterns exactly for visual consistency with the Home page):
|
|
5782
|
-
${lines.map((l) => ` - ${l}`).join("\n")}`;
|
|
5783
|
-
}
|
|
5784
|
-
async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
|
|
5785
|
-
let pageNames = [];
|
|
5786
|
-
spinner.start("Phase 1/4 \u2014 Planning pages...");
|
|
5787
|
-
try {
|
|
5788
|
-
const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
|
|
5789
|
-
const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
|
|
5790
|
-
pageNames = pageReqs.map((r) => {
|
|
5791
|
-
const c = r.changes;
|
|
5792
|
-
const name = c.name || c.id || "page";
|
|
5793
|
-
const id = c.id || name.toLowerCase().replace(/\s+/g, "-");
|
|
5794
|
-
const route = c.route || `/${id}`;
|
|
5795
|
-
return { name, id, route };
|
|
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"
|
|
5796
5381
|
});
|
|
5797
|
-
} catch {
|
|
5798
|
-
spinner.text = "AI plan failed \u2014 extracting pages from your request...";
|
|
5799
5382
|
}
|
|
5800
|
-
|
|
5801
|
-
|
|
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
|
+
});
|
|
5802
5391
|
}
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
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
|
+
});
|
|
5806
5401
|
}
|
|
5807
|
-
|
|
5808
|
-
const
|
|
5809
|
-
if (!
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
}
|
|
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
|
+
});
|
|
5817
5411
|
}
|
|
5818
|
-
const
|
|
5819
|
-
const
|
|
5820
|
-
if (
|
|
5821
|
-
|
|
5822
|
-
|
|
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
|
+
});
|
|
5823
5421
|
}
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
homePageCode = codePage.changes?.pageCode || "";
|
|
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
|
+
}
|
|
5441
|
+
}
|
|
5845
5442
|
}
|
|
5846
|
-
} catch {
|
|
5847
5443
|
}
|
|
5848
|
-
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5444
|
+
return issues;
|
|
5445
|
+
}
|
|
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");
|
|
5854
5453
|
}
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
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");
|
|
5462
|
+
}
|
|
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");
|
|
5863
5468
|
}
|
|
5864
|
-
if (
|
|
5865
|
-
|
|
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");
|
|
5866
5472
|
}
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
`Create ONE page called "${name}" at route "${route}".`,
|
|
5879
|
-
`Context: ${message}.`,
|
|
5880
|
-
`Generate complete pageCode for this single page only. Do not generate other pages.`,
|
|
5881
|
-
sharedNote,
|
|
5882
|
-
routeNote,
|
|
5883
|
-
alignmentNote,
|
|
5884
|
-
existingPagesContext,
|
|
5885
|
-
styleContext
|
|
5886
|
-
].filter(Boolean).join("\n\n");
|
|
5887
|
-
try {
|
|
5888
|
-
const result = await parseModification(prompt, modCtx, provider, parseOpts);
|
|
5889
|
-
phase4Done++;
|
|
5890
|
-
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
5891
|
-
const codePage = result.requests.find((r) => r.type === "add-page");
|
|
5892
|
-
return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
|
|
5893
|
-
} catch {
|
|
5894
|
-
phase4Done++;
|
|
5895
|
-
spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
|
|
5896
|
-
return { type: "add-page", target: "new", changes: { id, name, route } };
|
|
5897
|
-
}
|
|
5898
|
-
},
|
|
5899
|
-
AI_CONCURRENCY
|
|
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");
|
|
5476
|
+
}
|
|
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");
|
|
5480
|
+
}
|
|
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
|
|
5900
5484
|
);
|
|
5901
|
-
const
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
return allRequests;
|
|
5905
|
-
}
|
|
5906
|
-
|
|
5907
|
-
// src/commands/chat/modification-handler.ts
|
|
5908
|
-
import { resolve as resolve7 } from "path";
|
|
5909
|
-
import { mkdir as mkdir4 } from "fs/promises";
|
|
5910
|
-
import { dirname as dirname6 } from "path";
|
|
5911
|
-
import chalk11 from "chalk";
|
|
5912
|
-
import {
|
|
5913
|
-
getTemplateForPageType,
|
|
5914
|
-
loadManifest as loadManifest5,
|
|
5915
|
-
saveManifest,
|
|
5916
|
-
updateUsedIn,
|
|
5917
|
-
findSharedComponentByIdOrName,
|
|
5918
|
-
generateSharedComponent as generateSharedComponent3
|
|
5919
|
-
} from "@getcoherent/core";
|
|
5485
|
+
const hasUseClient = /^['"]use client['"]/.test(fixed.trim());
|
|
5486
|
+
if ((hasHooks || hasEvents) && !hasUseClient) {
|
|
5487
|
+
fixed = `'use client'
|
|
5920
5488
|
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
5924
|
-
var TEXT_BASE_RE = /\btext-base\b/g;
|
|
5925
|
-
var HEAVY_SHADOW_RE = /\bshadow-(md|lg|xl|2xl)\b/g;
|
|
5926
|
-
var SM_BREAKPOINT_RE = /\bsm:/g;
|
|
5927
|
-
var XL_BREAKPOINT_RE = /\bxl:/g;
|
|
5928
|
-
var XXL_BREAKPOINT_RE = /\b2xl:/g;
|
|
5929
|
-
var LARGE_CARD_TITLE_RE = /CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/g;
|
|
5930
|
-
var RAW_BUTTON_RE = /<button\b/g;
|
|
5931
|
-
var RAW_INPUT_RE = /<input\b/g;
|
|
5932
|
-
var RAW_SELECT_RE = /<select\b/g;
|
|
5933
|
-
var NATIVE_CHECKBOX_RE = /<input[^>]*type\s*=\s*["']checkbox["']/g;
|
|
5934
|
-
var NATIVE_TABLE_RE = /<table\b/g;
|
|
5935
|
-
var PLACEHOLDER_PATTERNS = [
|
|
5936
|
-
/>\s*Lorem ipsum\b/i,
|
|
5937
|
-
/>\s*Card content\s*</i,
|
|
5938
|
-
/>\s*Your (?:text|content) here\s*</i,
|
|
5939
|
-
/>\s*Description\s*</,
|
|
5940
|
-
/>\s*Title\s*</,
|
|
5941
|
-
/placeholder\s*text/i
|
|
5942
|
-
];
|
|
5943
|
-
var GENERIC_BUTTON_LABELS = />\s*(Submit|OK|Click here|Press here|Go)\s*</i;
|
|
5944
|
-
var IMG_WITHOUT_ALT_RE = /<img\b(?![^>]*\balt\s*=)[^>]*>/g;
|
|
5945
|
-
var INPUT_TAG_RE = /<(?:Input|input)\b[^>]*>/g;
|
|
5946
|
-
var LABEL_FOR_RE = /<Label\b[^>]*htmlFor\s*=/;
|
|
5947
|
-
function isInsideCommentOrString(line, matchIndex) {
|
|
5948
|
-
const commentIdx = line.indexOf("//");
|
|
5949
|
-
if (commentIdx !== -1 && commentIdx < matchIndex) return true;
|
|
5950
|
-
let inSingle = false;
|
|
5951
|
-
let inDouble = false;
|
|
5952
|
-
let inTemplate = false;
|
|
5953
|
-
for (let i = 0; i < matchIndex; i++) {
|
|
5954
|
-
const ch = line[i];
|
|
5955
|
-
const prev = i > 0 ? line[i - 1] : "";
|
|
5956
|
-
if (prev === "\\") continue;
|
|
5957
|
-
if (ch === "'" && !inDouble && !inTemplate) inSingle = !inSingle;
|
|
5958
|
-
if (ch === '"' && !inSingle && !inTemplate) inDouble = !inDouble;
|
|
5959
|
-
if (ch === "`" && !inSingle && !inDouble) inTemplate = !inTemplate;
|
|
5489
|
+
${fixed}`;
|
|
5490
|
+
fixes.push('added "use client" (client features detected)');
|
|
5960
5491
|
}
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
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")');
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
const lines = fixed.split("\n");
|
|
5511
|
+
let hasReplacedButton = false;
|
|
5967
5512
|
for (let i = 0; i < lines.length; i++) {
|
|
5968
|
-
|
|
5969
|
-
if (
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
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;
|
|
5520
|
+
}
|
|
5521
|
+
if (hasReplacedButton) {
|
|
5522
|
+
fixed = lines.join("\n");
|
|
5523
|
+
fixed = fixed.replace(/<\/button>/g, (_match, _offset) => {
|
|
5524
|
+
return "</Button>";
|
|
5525
|
+
});
|
|
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
|
+
});
|
|
5537
|
+
}
|
|
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
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
fixes.push("<button> \u2192 <Button> (with import)");
|
|
5550
|
+
}
|
|
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";
|
|
5624
|
+
}
|
|
5625
|
+
}
|
|
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";
|
|
5974
5638
|
}
|
|
5975
|
-
continue;
|
|
5976
5639
|
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
continue;
|
|
5640
|
+
if (prefix === "border") {
|
|
5641
|
+
hadColorFix = true;
|
|
5642
|
+
return isDestructive ? "border-destructive" : "border-primary";
|
|
5981
5643
|
}
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
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";
|
|
5988
5661
|
}
|
|
5989
5662
|
}
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
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;
|
|
5994
5717
|
}
|
|
5995
5718
|
}
|
|
5996
5719
|
}
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
"info"
|
|
6040
|
-
)
|
|
6041
|
-
);
|
|
6042
|
-
issues.push(
|
|
6043
|
-
...checkLines(
|
|
6044
|
-
code,
|
|
6045
|
-
XL_BREAKPOINT_RE,
|
|
6046
|
-
"XL_BREAKPOINT",
|
|
6047
|
-
"xl: breakpoint \u2014 consider if md:/lg: is sufficient",
|
|
6048
|
-
"info"
|
|
6049
|
-
)
|
|
6050
|
-
);
|
|
6051
|
-
issues.push(
|
|
6052
|
-
...checkLines(
|
|
6053
|
-
code,
|
|
6054
|
-
XXL_BREAKPOINT_RE,
|
|
6055
|
-
"XXL_BREAKPOINT",
|
|
6056
|
-
"2xl: breakpoint \u2014 rarely needed, consider xl: instead",
|
|
6057
|
-
"warning"
|
|
6058
|
-
)
|
|
6059
|
-
);
|
|
6060
|
-
issues.push(
|
|
6061
|
-
...checkLines(
|
|
6062
|
-
code,
|
|
6063
|
-
LARGE_CARD_TITLE_RE,
|
|
6064
|
-
"LARGE_CARD_TITLE",
|
|
6065
|
-
"Large text on CardTitle \u2014 use text-sm font-medium",
|
|
6066
|
-
"warning"
|
|
6067
|
-
)
|
|
6068
|
-
);
|
|
6069
|
-
const codeLines = code.split("\n");
|
|
6070
|
-
issues.push(
|
|
6071
|
-
...checkLines(
|
|
6072
|
-
code,
|
|
6073
|
-
RAW_BUTTON_RE,
|
|
6074
|
-
"NATIVE_BUTTON",
|
|
6075
|
-
"Native <button> \u2014 use Button from @/components/ui/button",
|
|
6076
|
-
"error",
|
|
6077
|
-
true
|
|
6078
|
-
).filter((issue) => {
|
|
6079
|
-
const nearby = codeLines.slice(Math.max(0, issue.line - 1), issue.line + 5).join(" ");
|
|
6080
|
-
if (nearby.includes("aria-label")) return false;
|
|
6081
|
-
if (/onClick=\{.*copy/i.test(nearby)) return false;
|
|
6082
|
-
return true;
|
|
6083
|
-
})
|
|
6084
|
-
);
|
|
6085
|
-
issues.push(
|
|
6086
|
-
...checkLines(
|
|
6087
|
-
code,
|
|
6088
|
-
RAW_SELECT_RE,
|
|
6089
|
-
"NATIVE_SELECT",
|
|
6090
|
-
"Native <select> \u2014 use Select from @/components/ui/select",
|
|
6091
|
-
"error",
|
|
6092
|
-
true
|
|
6093
|
-
)
|
|
6094
|
-
);
|
|
6095
|
-
issues.push(
|
|
6096
|
-
...checkLines(
|
|
6097
|
-
code,
|
|
6098
|
-
NATIVE_CHECKBOX_RE,
|
|
6099
|
-
"NATIVE_CHECKBOX",
|
|
6100
|
-
'Native <input type="checkbox"> \u2014 use Switch or Checkbox from @/components/ui/switch or @/components/ui/checkbox',
|
|
6101
|
-
"error",
|
|
6102
|
-
true
|
|
6103
|
-
)
|
|
6104
|
-
);
|
|
6105
|
-
issues.push(
|
|
6106
|
-
...checkLines(
|
|
6107
|
-
code,
|
|
6108
|
-
NATIVE_TABLE_RE,
|
|
6109
|
-
"NATIVE_TABLE",
|
|
6110
|
-
"Native <table> \u2014 use Table, TableHeader, TableBody, etc. from @/components/ui/table",
|
|
6111
|
-
"warning",
|
|
6112
|
-
true
|
|
6113
|
-
)
|
|
6114
|
-
);
|
|
6115
|
-
const hasInputImport = /import\s.*Input.*from\s+['"]@\/components\/ui\//.test(code);
|
|
6116
|
-
if (!hasInputImport) {
|
|
6117
|
-
issues.push(
|
|
6118
|
-
...checkLines(
|
|
6119
|
-
code,
|
|
6120
|
-
RAW_INPUT_RE,
|
|
6121
|
-
"RAW_INPUT",
|
|
6122
|
-
"Raw <input> element \u2014 import and use Input from @/components/ui/input",
|
|
6123
|
-
"warning",
|
|
6124
|
-
true
|
|
6125
|
-
)
|
|
6126
|
-
);
|
|
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
|
+
}
|
|
6127
5762
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
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(", ")}`);
|
|
6138
5812
|
}
|
|
6139
5813
|
}
|
|
6140
5814
|
}
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
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`
|
|
6149
5875
|
});
|
|
6150
5876
|
}
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
"GENERIC_BUTTON_TEXT",
|
|
6165
|
-
'Generic button text \u2014 use specific verb ("Save changes", "Delete account")',
|
|
6166
|
-
"warning"
|
|
6167
|
-
)
|
|
6168
|
-
);
|
|
6169
|
-
const h1Matches = code.match(/<h1[\s>]/g);
|
|
6170
|
-
if (!h1Matches || h1Matches.length === 0) {
|
|
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"')) {
|
|
6171
5890
|
issues.push({
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
message: "Page has no <h1> \u2014 every page should have exactly one h1 heading",
|
|
6175
|
-
severity: "warning"
|
|
5891
|
+
type: "missing-use-client",
|
|
5892
|
+
message: 'Code uses React hooks but missing "use client" directive'
|
|
6176
5893
|
});
|
|
6177
|
-
}
|
|
5894
|
+
}
|
|
5895
|
+
if (!after.includes("export default")) {
|
|
6178
5896
|
issues.push({
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
message: `Page has ${h1Matches.length} <h1> elements \u2014 use exactly one per page`,
|
|
6182
|
-
severity: "warning"
|
|
5897
|
+
type: "missing-default-export",
|
|
5898
|
+
message: "Missing default export \u2014 page component must have a default export"
|
|
6183
5899
|
});
|
|
6184
5900
|
}
|
|
6185
|
-
const
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
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()));
|
|
5906
|
+
}
|
|
5907
|
+
for (const match of after.matchAll(importRegex)) {
|
|
5908
|
+
match[1].split(",").forEach((s) => afterImports.add(s.trim()));
|
|
5909
|
+
}
|
|
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
|
+
}
|
|
6195
5921
|
}
|
|
6196
5922
|
}
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
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");
|
|
6207
5940
|
}
|
|
6208
|
-
if (!
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
5941
|
+
if (!slug) {
|
|
5942
|
+
return resolve5(projectRoot, "app", "page.tsx");
|
|
5943
|
+
}
|
|
5944
|
+
if (isMarketingRoute(route)) {
|
|
5945
|
+
return resolve5(projectRoot, "app", slug, "page.tsx");
|
|
5946
|
+
}
|
|
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`;
|
|
5953
|
+
}
|
|
5954
|
+
if (!slug) {
|
|
5955
|
+
return "app/page.tsx";
|
|
5956
|
+
}
|
|
5957
|
+
if (isMarketingRoute(route)) {
|
|
5958
|
+
return `app/${slug}/page.tsx`;
|
|
5959
|
+
}
|
|
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);
|
|
6215
5981
|
}
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
});
|
|
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
|
+
);
|
|
6017
|
+
}
|
|
6018
|
+
} catch {
|
|
6019
|
+
}
|
|
6224
6020
|
}
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
message: "Interactive elements without focus-visible styles \u2014 add focus-visible:ring-2 focus-visible:ring-ring",
|
|
6233
|
-
severity: "info"
|
|
6234
|
-
});
|
|
6021
|
+
}
|
|
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
|
+
);
|
|
6235
6028
|
}
|
|
6236
|
-
const
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
message: "List/table/grid without empty state handling \u2014 add friendly message + primary action",
|
|
6245
|
-
severity: "warning"
|
|
6246
|
-
});
|
|
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();
|
|
6247
6037
|
}
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
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"));
|
|
6257
6066
|
}
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
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"));
|
|
6266
6091
|
}
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
issues.push({
|
|
6271
|
-
line: 0,
|
|
6272
|
-
type: "DESTRUCTIVE_NO_CONFIRM",
|
|
6273
|
-
message: "Destructive action without confirmation dialog \u2014 add confirm before execution",
|
|
6274
|
-
severity: "warning"
|
|
6275
|
-
});
|
|
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.`;
|
|
6276
6095
|
}
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6096
|
+
return message;
|
|
6097
|
+
}
|
|
6098
|
+
|
|
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" }]
|
|
6286
6128
|
}
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
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);
|
|
6296
6139
|
}
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
if (
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
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
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
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
|
+
}
|
|
6315
6164
|
}
|
|
6316
6165
|
}
|
|
6317
6166
|
}
|
|
6318
6167
|
}
|
|
6319
|
-
return
|
|
6168
|
+
return inferred;
|
|
6320
6169
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
const beforeQuoteFix = fixed;
|
|
6325
|
-
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
6326
|
-
if (fixed !== beforeQuoteFix) {
|
|
6327
|
-
fixes.push("fixed escaped closing quotes in strings");
|
|
6328
|
-
}
|
|
6329
|
-
const beforeEntityFix = fixed;
|
|
6330
|
-
fixed = fixed.replace(/<=/g, "<=");
|
|
6331
|
-
fixed = fixed.replace(/>=/g, ">=");
|
|
6332
|
-
fixed = fixed.replace(/&&/g, "&&");
|
|
6333
|
-
fixed = fixed.replace(/([\w)\]])\s*<\s*([\w(])/g, "$1 < $2");
|
|
6334
|
-
fixed = fixed.replace(/([\w)\]])\s*>\s*([\w(])/g, "$1 > $2");
|
|
6335
|
-
if (fixed !== beforeEntityFix) {
|
|
6336
|
-
fixes.push("Fixed syntax issues");
|
|
6337
|
-
}
|
|
6338
|
-
const beforeLtFix = fixed;
|
|
6339
|
-
fixed = fixed.replace(/>([^<{}\n]*)<(\d)/g, ">$1<$2");
|
|
6340
|
-
fixed = fixed.replace(/>([^<{}\n]*)<([^/a-zA-Z!{>\n])/g, ">$1<$2");
|
|
6341
|
-
if (fixed !== beforeLtFix) {
|
|
6342
|
-
fixes.push("escaped < in JSX text content");
|
|
6343
|
-
}
|
|
6344
|
-
if (/className="[^"]*\btext-base\b[^"]*"/.test(fixed)) {
|
|
6345
|
-
fixed = fixed.replace(/className="([^"]*)\btext-base\b([^"]*)"/g, 'className="$1text-sm$2"');
|
|
6346
|
-
fixes.push("text-base \u2192 text-sm");
|
|
6347
|
-
}
|
|
6348
|
-
if (/CardTitle[^>]*className="[^"]*text-(lg|xl|2xl)/.test(fixed)) {
|
|
6349
|
-
fixed = fixed.replace(/(CardTitle[^>]*className="[^"]*)text-(lg|xl|2xl)\b/g, "$1");
|
|
6350
|
-
fixes.push("large text in CardTitle \u2192 removed");
|
|
6351
|
-
}
|
|
6352
|
-
if (/className="[^"]*\bshadow-(md|lg|xl|2xl)\b[^"]*"/.test(fixed)) {
|
|
6353
|
-
fixed = fixed.replace(/className="([^"]*)\bshadow-(md|lg|xl|2xl)\b([^"]*)"/g, 'className="$1shadow-sm$3"');
|
|
6354
|
-
fixes.push("heavy shadow \u2192 shadow-sm");
|
|
6355
|
-
}
|
|
6356
|
-
const hasHooks = /\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\b/.test(fixed);
|
|
6357
|
-
const hasEvents = /\b(onClick|onChange|onSubmit|onBlur|onFocus|onKeyDown|onKeyUp|onMouseEnter|onMouseLeave|onScroll|onInput)\s*[={]/.test(
|
|
6358
|
-
fixed
|
|
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
|
|
6359
6173
|
);
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
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 });
|
|
6377
6210
|
}
|
|
6378
|
-
const tail = fixed.slice(i);
|
|
6379
|
-
const semi = tail.match(/^\s*;/);
|
|
6380
|
-
const removeEnd = semi ? i + (semi.index + semi[0].length) : i;
|
|
6381
|
-
fixed = (fixed.slice(0, start) + fixed.slice(removeEnd)).replace(/\n{3,}/g, "\n\n").trim();
|
|
6382
|
-
fixes.push('removed metadata export (conflicts with "use client")');
|
|
6383
6211
|
}
|
|
6384
6212
|
}
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
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(", ")}` };
|
|
6395
6231
|
}
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
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
|
+
};
|
|
6252
|
+
}
|
|
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"}` };
|
|
6256
|
+
}
|
|
6257
|
+
if (page.id !== request.target) {
|
|
6258
|
+
return { ...request, target: page.id };
|
|
6422
6259
|
}
|
|
6260
|
+
break;
|
|
6423
6261
|
}
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
"bg-zinc-100": "bg-muted",
|
|
6437
|
-
"bg-slate-100": "bg-muted",
|
|
6438
|
-
"bg-gray-100": "bg-muted",
|
|
6439
|
-
"bg-white": "bg-background",
|
|
6440
|
-
"bg-black": "bg-background",
|
|
6441
|
-
"text-white": "text-foreground",
|
|
6442
|
-
"text-black": "text-foreground",
|
|
6443
|
-
"text-zinc-100": "text-foreground",
|
|
6444
|
-
"text-zinc-200": "text-foreground",
|
|
6445
|
-
"text-slate-100": "text-foreground",
|
|
6446
|
-
"text-gray-100": "text-foreground",
|
|
6447
|
-
"text-zinc-400": "text-muted-foreground",
|
|
6448
|
-
"text-zinc-500": "text-muted-foreground",
|
|
6449
|
-
"text-slate-400": "text-muted-foreground",
|
|
6450
|
-
"text-slate-500": "text-muted-foreground",
|
|
6451
|
-
"text-gray-400": "text-muted-foreground",
|
|
6452
|
-
"text-gray-500": "text-muted-foreground",
|
|
6453
|
-
"border-zinc-700": "border-border",
|
|
6454
|
-
"border-zinc-800": "border-border",
|
|
6455
|
-
"border-slate-700": "border-border",
|
|
6456
|
-
"border-gray-700": "border-border",
|
|
6457
|
-
"border-zinc-200": "border-border",
|
|
6458
|
-
"border-slate-200": "border-border",
|
|
6459
|
-
"border-gray-200": "border-border"
|
|
6460
|
-
};
|
|
6461
|
-
const isCodeContext = (classes) => /\bfont-mono\b/.test(classes) || /\bbg-zinc-950\b/.test(classes) || /\bbg-zinc-900\b/.test(classes);
|
|
6462
|
-
const isInsideTerminalBlock = (offset) => {
|
|
6463
|
-
const preceding = fixed.slice(Math.max(0, offset - 600), offset);
|
|
6464
|
-
if (!/(bg-zinc-950|bg-zinc-900)/.test(preceding)) return false;
|
|
6465
|
-
if (!/font-mono/.test(preceding)) return false;
|
|
6466
|
-
const lastClose = Math.max(preceding.lastIndexOf("</div>"), preceding.lastIndexOf("</section>"));
|
|
6467
|
-
const lastTerminal = Math.max(preceding.lastIndexOf("bg-zinc-950"), preceding.lastIndexOf("bg-zinc-900"));
|
|
6468
|
-
return lastTerminal > lastClose;
|
|
6469
|
-
};
|
|
6470
|
-
let hadColorFix = false;
|
|
6471
|
-
fixed = fixed.replace(/className="([^"]*)"/g, (fullMatch, classes, offset) => {
|
|
6472
|
-
if (isCodeContext(classes)) return fullMatch;
|
|
6473
|
-
if (isInsideTerminalBlock(offset)) return fullMatch;
|
|
6474
|
-
let result = classes;
|
|
6475
|
-
const accentColorRe = /\b(bg|text|border)-(emerald|blue|violet|indigo|purple|teal|cyan|sky|rose|amber)-(\d+)\b/g;
|
|
6476
|
-
result = result.replace(accentColorRe, (m, prefix, _color, shade) => {
|
|
6477
|
-
if (colorMap[m]) {
|
|
6478
|
-
hadColorFix = true;
|
|
6479
|
-
return colorMap[m];
|
|
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
|
+
};
|
|
6480
6274
|
}
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6275
|
+
if (!changes.id && changes.name) {
|
|
6276
|
+
changes.id = String(changes.name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
|
|
6277
|
+
}
|
|
6278
|
+
if (!changes.id && route) {
|
|
6279
|
+
changes.id = route.replace(/^\//, "") || "home";
|
|
6280
|
+
}
|
|
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
|
+
};
|
|
6486
6302
|
}
|
|
6487
|
-
if (
|
|
6488
|
-
|
|
6489
|
-
|
|
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
|
+
}
|
|
6490
6311
|
}
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6312
|
+
}
|
|
6313
|
+
break;
|
|
6314
|
+
}
|
|
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;
|
|
6494
6321
|
}
|
|
6495
6322
|
}
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
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"}` };
|
|
6500
6331
|
}
|
|
6501
|
-
if (
|
|
6502
|
-
|
|
6503
|
-
return "text-foreground";
|
|
6332
|
+
if (page.id !== request.target) {
|
|
6333
|
+
return { ...request, target: page.id };
|
|
6504
6334
|
}
|
|
6505
6335
|
}
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6336
|
+
break;
|
|
6337
|
+
}
|
|
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
|
+
};
|
|
6509
6345
|
}
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
const neutralColorRe = /\b(bg|text|border)-(zinc|slate|gray|neutral|stone)-(\d+)\b/g;
|
|
6513
|
-
result = result.replace(neutralColorRe, (m, prefix, _color, shade) => {
|
|
6514
|
-
if (colorMap[m]) {
|
|
6515
|
-
hadColorFix = true;
|
|
6516
|
-
return colorMap[m];
|
|
6346
|
+
if (sourcePage.id !== request.target) {
|
|
6347
|
+
return { ...request, target: sourcePage.id };
|
|
6517
6348
|
}
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6349
|
+
break;
|
|
6350
|
+
}
|
|
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 } : {}
|
|
6528
6390
|
}
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
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
|
+
};
|
|
6533
6406
|
}
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
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: "" };
|
|
6537
6416
|
}
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
if (result !== classes) return `className="${result}"`;
|
|
6546
|
-
return fullMatch;
|
|
6547
|
-
});
|
|
6548
|
-
if (hadColorFix) fixes.push("raw colors \u2192 semantic tokens");
|
|
6549
|
-
const lucideImportMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
6550
|
-
if (lucideImportMatch) {
|
|
6551
|
-
let lucideExports = null;
|
|
6552
|
-
try {
|
|
6553
|
-
const { createRequire } = await import("module");
|
|
6554
|
-
const require2 = createRequire(process.cwd() + "/package.json");
|
|
6555
|
-
const lr = require2("lucide-react");
|
|
6556
|
-
lucideExports = new Set(Object.keys(lr).filter((k) => /^[A-Z]/.test(k)));
|
|
6557
|
-
} catch {
|
|
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
|
+
});
|
|
6558
6424
|
}
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
for (const dup of duplicates) {
|
|
6568
|
-
newImport = newImport.replace(new RegExp(`\\b${dup}\\b,?\\s*`), "");
|
|
6569
|
-
fixes.push(`removed ${dup} from lucide import (conflicts with UI component import)`);
|
|
6425
|
+
return {
|
|
6426
|
+
...request,
|
|
6427
|
+
changes: {
|
|
6428
|
+
...changes,
|
|
6429
|
+
variants: normalizedVariants,
|
|
6430
|
+
sizes: normalizedSizes,
|
|
6431
|
+
createdAt: now,
|
|
6432
|
+
updatedAt: now
|
|
6570
6433
|
}
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
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 ?? "" };
|
|
6578
6463
|
}
|
|
6579
|
-
|
|
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 }
|
|
6580
6473
|
}
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
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 });
|
|
6589
6517
|
}
|
|
6590
6518
|
}
|
|
6591
6519
|
}
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
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(", ")}`);
|
|
6601
6570
|
}
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6606
|
-
|
|
6607
|
-
|
|
6608
|
-
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
const baseName = ref.replace(/Icon$/, "");
|
|
6619
|
-
if (lucideExports2.has(ref)) {
|
|
6620
|
-
missing.push(ref);
|
|
6621
|
-
lucideImported.add(ref);
|
|
6622
|
-
} else if (lucideExports2.has(baseName)) {
|
|
6623
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6624
|
-
fixed = fixed.replace(re, baseName);
|
|
6625
|
-
missing.push(baseName);
|
|
6626
|
-
lucideImported.add(baseName);
|
|
6627
|
-
fixes.push(`renamed ${ref} \u2192 ${baseName} (lucide-react)`);
|
|
6628
|
-
} else {
|
|
6629
|
-
const fallback = "Circle";
|
|
6630
|
-
const re = new RegExp(`\\b${ref}\\b`, "g");
|
|
6631
|
-
fixed = fixed.replace(re, fallback);
|
|
6632
|
-
lucideImported.add(fallback);
|
|
6633
|
-
fixes.push(`unknown icon ${ref} \u2192 ${fallback}`);
|
|
6634
|
-
}
|
|
6635
|
-
}
|
|
6636
|
-
if (missing.length > 0) {
|
|
6637
|
-
const allNames = [...lucideImported];
|
|
6638
|
-
const origLine = lucideImportMatch2[0];
|
|
6639
|
-
fixed = fixed.replace(origLine, `import { ${allNames.join(", ")} } from "lucide-react"`);
|
|
6640
|
-
fixes.push(`added missing lucide imports: ${missing.join(", ")}`);
|
|
6641
|
-
}
|
|
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);
|
|
6642
6587
|
}
|
|
6643
6588
|
}
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
fixed = fixed.replace(/["']https?:\/\/via\.placeholder\.com\/(\d+)x?(\d*)(?:\/[^"']*)?\/?["']/g, (_m, w, h) => {
|
|
6657
|
-
const height = h || w;
|
|
6658
|
-
return `"https://picsum.photos/${w}/${height}?random=${imgCounter++}"`;
|
|
6659
|
-
});
|
|
6660
|
-
fixed = fixed.replace(/["']\/images\/[^"']+\.(?:jpg|jpeg|png|webp|gif)["']/g, () => {
|
|
6661
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
6662
|
-
});
|
|
6663
|
-
fixed = fixed.replace(/["']\/placeholder[^"']*\.(?:jpg|jpeg|png|webp)["']/g, () => {
|
|
6664
|
-
return `"https://picsum.photos/800/400?random=${imgCounter++}"`;
|
|
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);
|
|
6665
6601
|
});
|
|
6666
|
-
|
|
6667
|
-
|
|
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`);
|
|
6668
6651
|
}
|
|
6669
|
-
|
|
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")}`;
|
|
6670
6664
|
}
|
|
6671
|
-
function
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
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...";
|
|
6682
6680
|
}
|
|
6683
|
-
if (
|
|
6684
|
-
|
|
6685
|
-
for (const w of warnings) {
|
|
6686
|
-
lines.push(` L${w.line}: [${w.type}] ${w.message}`);
|
|
6687
|
-
}
|
|
6681
|
+
if (pageNames.length === 0) {
|
|
6682
|
+
pageNames = extractPageNamesFromMessage(message);
|
|
6688
6683
|
}
|
|
6689
|
-
if (
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
lines.push(` L${i.line}: [${i.type}] ${i.message}`);
|
|
6693
|
-
}
|
|
6684
|
+
if (pageNames.length === 0) {
|
|
6685
|
+
spinner.fail("Could not determine pages to create");
|
|
6686
|
+
return [];
|
|
6694
6687
|
}
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
}
|
|
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
|
+
}
|
|
6705
6698
|
}
|
|
6706
|
-
const
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
});
|
|
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);
|
|
6712
6704
|
}
|
|
6713
|
-
|
|
6714
|
-
}
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
const
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
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 || "";
|
|
6726
|
+
}
|
|
6727
|
+
} catch {
|
|
6723
6728
|
}
|
|
6724
|
-
if (!
|
|
6725
|
-
|
|
6726
|
-
type: "
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
+
if (!homeRequest) {
|
|
6730
|
+
homeRequest = {
|
|
6731
|
+
type: "add-page",
|
|
6732
|
+
target: "new",
|
|
6733
|
+
changes: { id: homePage.id, name: homePage.name, route: homePage.route }
|
|
6734
|
+
};
|
|
6729
6735
|
}
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
const
|
|
6733
|
-
|
|
6734
|
-
|
|
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)");
|
|
6735
6744
|
}
|
|
6736
|
-
|
|
6737
|
-
|
|
6745
|
+
if (remainingPages.length === 0) {
|
|
6746
|
+
return [homeRequest];
|
|
6738
6747
|
}
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
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 {
|
|
6749
6803
|
}
|
|
6750
6804
|
}
|
|
6751
6805
|
}
|
|
6752
|
-
|
|
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;
|
|
6753
6809
|
}
|
|
6754
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
|
+
|
|
6755
6825
|
// src/commands/chat/code-generator.ts
|
|
6756
6826
|
import { resolve as resolve6 } from "path";
|
|
6757
6827
|
import { existsSync as existsSync14 } from "fs";
|
|
@@ -7769,10 +7839,9 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7769
7839
|
layoutShared: manifestForAudit.shared.filter((c) => c.type === "layout"),
|
|
7770
7840
|
allShared: manifestForAudit.shared
|
|
7771
7841
|
});
|
|
7772
|
-
const
|
|
7773
|
-
const issues = validatePageQuality(codeToWrite, validRoutes);
|
|
7842
|
+
const issues = validatePageQuality(codeToWrite);
|
|
7774
7843
|
const errors = issues.filter((i) => i.severity === "error");
|
|
7775
|
-
if (errors.length >=
|
|
7844
|
+
if (errors.length >= 2 && aiProvider) {
|
|
7776
7845
|
console.log(
|
|
7777
7846
|
chalk11.yellow(`
|
|
7778
7847
|
\u{1F504} ${errors.length} quality errors \u2014 attempting AI fix for ${page.name || page.id}...`)
|
|
@@ -7791,7 +7860,7 @@ Rules:
|
|
|
7791
7860
|
- Keep all existing functionality and layout intact`;
|
|
7792
7861
|
const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
|
|
7793
7862
|
if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
|
|
7794
|
-
const recheck = validatePageQuality(fixedCode2
|
|
7863
|
+
const recheck = validatePageQuality(fixedCode2);
|
|
7795
7864
|
const recheckErrors = recheck.filter((i) => i.severity === "error");
|
|
7796
7865
|
if (recheckErrors.length < errors.length) {
|
|
7797
7866
|
codeToWrite = fixedCode2;
|
|
@@ -8479,18 +8548,18 @@ async function chatCommand(message, options) {
|
|
|
8479
8548
|
);
|
|
8480
8549
|
const preflightInstalledIds = [];
|
|
8481
8550
|
const allNpmImportsFromPages = /* @__PURE__ */ new Set();
|
|
8551
|
+
const allNeededComponentIds = /* @__PURE__ */ new Set();
|
|
8482
8552
|
for (const pageRequest of pageRequests) {
|
|
8483
8553
|
const page = pageRequest.changes;
|
|
8484
|
-
const neededComponentIds = /* @__PURE__ */ new Set();
|
|
8485
8554
|
page.sections?.forEach(
|
|
8486
8555
|
(section) => {
|
|
8487
8556
|
if (section.componentId) {
|
|
8488
|
-
|
|
8557
|
+
allNeededComponentIds.add(section.componentId);
|
|
8489
8558
|
}
|
|
8490
8559
|
if (section.props?.fields && Array.isArray(section.props.fields)) {
|
|
8491
8560
|
section.props.fields.forEach((field) => {
|
|
8492
8561
|
if (field.component) {
|
|
8493
|
-
|
|
8562
|
+
allNeededComponentIds.add(field.component);
|
|
8494
8563
|
}
|
|
8495
8564
|
});
|
|
8496
8565
|
}
|
|
@@ -8499,7 +8568,7 @@ async function chatCommand(message, options) {
|
|
|
8499
8568
|
if (typeof page.pageCode === "string" && page.pageCode.trim() !== "") {
|
|
8500
8569
|
const importMatches = page.pageCode.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
|
|
8501
8570
|
for (const m of importMatches) {
|
|
8502
|
-
if (m[1])
|
|
8571
|
+
if (m[1]) allNeededComponentIds.add(m[1]);
|
|
8503
8572
|
}
|
|
8504
8573
|
extractNpmPackagesFromCode(page.pageCode).forEach((p) => allNpmImportsFromPages.add(p));
|
|
8505
8574
|
}
|
|
@@ -8514,7 +8583,7 @@ async function chatCommand(message, options) {
|
|
|
8514
8583
|
});
|
|
8515
8584
|
const tmplImports = preview.matchAll(/@\/components\/ui\/([a-z0-9-]+)/g);
|
|
8516
8585
|
for (const m of tmplImports) {
|
|
8517
|
-
if (m[1])
|
|
8586
|
+
if (m[1]) allNeededComponentIds.add(m[1]);
|
|
8518
8587
|
}
|
|
8519
8588
|
extractNpmPackagesFromCode(preview).forEach((p) => allNpmImportsFromPages.add(p));
|
|
8520
8589
|
} catch {
|
|
@@ -8522,8 +8591,8 @@ async function chatCommand(message, options) {
|
|
|
8522
8591
|
}
|
|
8523
8592
|
}
|
|
8524
8593
|
if (DEBUG4) {
|
|
8525
|
-
console.log(chalk13.gray(
|
|
8526
|
-
|
|
8594
|
+
console.log(chalk13.gray(`
|
|
8595
|
+
[DEBUG] Pre-flight analysis for page "${page.name || page.route}": `));
|
|
8527
8596
|
console.log(chalk13.gray(` Page sections: ${page.sections?.length || 0}`));
|
|
8528
8597
|
if (page.sections?.[0]?.props?.fields) {
|
|
8529
8598
|
console.log(chalk13.gray(` First section has ${page.sections[0].props.fields.length} fields`));
|
|
@@ -8531,64 +8600,67 @@ async function chatCommand(message, options) {
|
|
|
8531
8600
|
console.log(chalk13.gray(` Field ${i}: component=${f.component}`));
|
|
8532
8601
|
});
|
|
8533
8602
|
}
|
|
8534
|
-
console.log("");
|
|
8535
8603
|
}
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
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);
|
|
8545
8618
|
}
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
}
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
}
|
|
8569
|
-
if (result.success) {
|
|
8570
|
-
preflightInstalledIds.push(shadcnDef.id);
|
|
8571
|
-
console.log(chalk13.green(` \u2728 Auto-installed ${shadcnDef.name} component`));
|
|
8572
|
-
const updatedConfig2 = result.config;
|
|
8573
|
-
dsm.updateConfig(updatedConfig2);
|
|
8574
|
-
cm.updateConfig(updatedConfig2);
|
|
8575
|
-
pm.updateConfig(updatedConfig2);
|
|
8576
|
-
}
|
|
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
|
+
);
|
|
8577
8641
|
}
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
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);
|
|
8583
8649
|
}
|
|
8584
8650
|
}
|
|
8585
|
-
}
|
|
8586
|
-
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
|
+
}
|
|
8587
8657
|
}
|
|
8658
|
+
} else {
|
|
8659
|
+
console.log(chalk13.yellow(` \u26A0\uFE0F Component ${componentId} not available`));
|
|
8588
8660
|
}
|
|
8589
|
-
console.log("");
|
|
8590
|
-
spinner.start("Applying modifications...");
|
|
8591
8661
|
}
|
|
8662
|
+
console.log("");
|
|
8663
|
+
spinner.start("Applying modifications...");
|
|
8592
8664
|
}
|
|
8593
8665
|
const installedPkgs = getInstalledPackages(projectRoot);
|
|
8594
8666
|
const neededPkgs = /* @__PURE__ */ new Set([...COHERENT_REQUIRED_PACKAGES, ...allNpmImportsFromPages]);
|
|
@@ -8750,6 +8822,34 @@ async function chatCommand(message, options) {
|
|
|
8750
8822
|
spinner.start("Finalizing...");
|
|
8751
8823
|
}
|
|
8752
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
|
+
}
|
|
8753
8853
|
const updatedConfig = dsm.getConfig();
|
|
8754
8854
|
const darkMatch = /\bdark\s*(theme|mode|background)\b/i.test(message);
|
|
8755
8855
|
const lightMatch = /\blight\s*(theme|mode|background)\b/i.test(message);
|