@getcoherent/cli 0.5.7 → 0.5.9
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 +275 -15
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -657,7 +657,8 @@ ${out}`;
|
|
|
657
657
|
return out;
|
|
658
658
|
}
|
|
659
659
|
function sanitizeMetadataStrings(code) {
|
|
660
|
-
let out = code.replace(
|
|
660
|
+
let out = code.replace(/\\'(\s*[}\],])/g, "'$1");
|
|
661
|
+
out = out.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
661
662
|
for (const key of ["description", "title"]) {
|
|
662
663
|
const re = new RegExp(`\\b${key}:\\s*'((?:[^'\\\\]|'(?![,}]))*)'`, "gs");
|
|
663
664
|
out = out.replace(re, (_, inner) => `${key}: '${inner.replace(/'/g, "\\'")}'`);
|
|
@@ -665,7 +666,9 @@ function sanitizeMetadataStrings(code) {
|
|
|
665
666
|
return out;
|
|
666
667
|
}
|
|
667
668
|
function fixEscapedClosingQuotes(code) {
|
|
668
|
-
|
|
669
|
+
let out = code.replace(/\\'(\s*[}\],])/g, "'$1");
|
|
670
|
+
out = out.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
671
|
+
return out;
|
|
669
672
|
}
|
|
670
673
|
function fixUnescapedLtInJsx(code) {
|
|
671
674
|
let out = code;
|
|
@@ -3598,6 +3601,7 @@ LINKS & INTERACTIVE STATES (consistency is critical):
|
|
|
3598
3601
|
|
|
3599
3602
|
ICONS:
|
|
3600
3603
|
- Size: ALWAYS size-4 (16px). Color: ALWAYS text-muted-foreground. Import: ALWAYS from lucide-react.
|
|
3604
|
+
- ALWAYS add shrink-0 to icon className to prevent flex containers from squishing them.
|
|
3601
3605
|
|
|
3602
3606
|
ANTI-PATTERNS (NEVER DO):
|
|
3603
3607
|
- text-base as body text \u2192 use text-sm
|
|
@@ -3609,6 +3613,16 @@ ANTI-PATTERNS (NEVER DO):
|
|
|
3609
3613
|
- Mixing link styles on the same page \u2192 pick ONE style
|
|
3610
3614
|
- Interactive elements without hover/focus states
|
|
3611
3615
|
|
|
3616
|
+
COMPONENT VARIANT RULES (CRITICAL):
|
|
3617
|
+
- NEVER use <Button> with custom bg-*/text-* classes for navigation or tabs without variant="ghost".
|
|
3618
|
+
The default Button variant sets bg-primary, so custom text-muted-foreground or bg-accent classes will conflict.
|
|
3619
|
+
BAD: <Button className="text-muted-foreground hover:bg-accent">Tab</Button>
|
|
3620
|
+
GOOD: <Button variant="ghost" className="text-muted-foreground">Tab</Button>
|
|
3621
|
+
BEST: Use shadcn <Tabs> / <TabsList> / <TabsTrigger> for tab-style navigation.
|
|
3622
|
+
- For sidebar navigation buttons, ALWAYS use variant="ghost" with active-state classes:
|
|
3623
|
+
<Button variant="ghost" className={cn("w-full justify-start", isActive && "bg-accent font-medium")}>
|
|
3624
|
+
- For filter toggle buttons, use variant={isActive ? 'default' : 'outline'} \u2014 NOT className toggling.
|
|
3625
|
+
|
|
3612
3626
|
CONTENT (zero placeholders):
|
|
3613
3627
|
- NEVER: "Lorem ipsum", "Card content", "Description here"
|
|
3614
3628
|
- ALWAYS: Real, contextual content. Realistic metric names, values, dates.
|
|
@@ -5056,6 +5070,121 @@ function fixGlobalsCss(projectRoot, config2) {
|
|
|
5056
5070
|
writeFileSync7(layoutPath, layoutContent, "utf-8");
|
|
5057
5071
|
}
|
|
5058
5072
|
|
|
5073
|
+
// src/utils/component-rules.ts
|
|
5074
|
+
function extractJsxElementProps(code, openTagStart) {
|
|
5075
|
+
let i = openTagStart;
|
|
5076
|
+
if (code[i] !== "<") return null;
|
|
5077
|
+
i++;
|
|
5078
|
+
let braceDepth = 0;
|
|
5079
|
+
let inSingleQuote = false;
|
|
5080
|
+
let inDoubleQuote = false;
|
|
5081
|
+
let inTemplateLiteral = false;
|
|
5082
|
+
let escaped = false;
|
|
5083
|
+
while (i < code.length) {
|
|
5084
|
+
const ch = code[i];
|
|
5085
|
+
if (escaped) {
|
|
5086
|
+
escaped = false;
|
|
5087
|
+
i++;
|
|
5088
|
+
continue;
|
|
5089
|
+
}
|
|
5090
|
+
if (ch === "\\" && (inSingleQuote || inDoubleQuote || inTemplateLiteral)) {
|
|
5091
|
+
escaped = true;
|
|
5092
|
+
i++;
|
|
5093
|
+
continue;
|
|
5094
|
+
}
|
|
5095
|
+
if (!inDoubleQuote && !inTemplateLiteral && ch === "'" && braceDepth > 0) {
|
|
5096
|
+
inSingleQuote = !inSingleQuote;
|
|
5097
|
+
} else if (!inSingleQuote && !inTemplateLiteral && ch === '"') {
|
|
5098
|
+
inDoubleQuote = !inDoubleQuote;
|
|
5099
|
+
} else if (!inSingleQuote && !inDoubleQuote && ch === "`") {
|
|
5100
|
+
inTemplateLiteral = !inTemplateLiteral;
|
|
5101
|
+
}
|
|
5102
|
+
if (!inSingleQuote && !inDoubleQuote && !inTemplateLiteral) {
|
|
5103
|
+
if (ch === "{") braceDepth++;
|
|
5104
|
+
else if (ch === "}") braceDepth--;
|
|
5105
|
+
else if (ch === ">" && braceDepth === 0) {
|
|
5106
|
+
return code.slice(openTagStart, i + 1);
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
i++;
|
|
5110
|
+
}
|
|
5111
|
+
return null;
|
|
5112
|
+
}
|
|
5113
|
+
var NAV_STYLE_SIGNAL = /text-muted-foreground/;
|
|
5114
|
+
var buttonMissingGhostVariant = {
|
|
5115
|
+
id: "button-missing-ghost-variant",
|
|
5116
|
+
component: "Button",
|
|
5117
|
+
detect(code) {
|
|
5118
|
+
const issues = [];
|
|
5119
|
+
const buttonRe = /<Button\s/g;
|
|
5120
|
+
let match;
|
|
5121
|
+
while ((match = buttonRe.exec(code)) !== null) {
|
|
5122
|
+
const props = extractJsxElementProps(code, match.index);
|
|
5123
|
+
if (!props) continue;
|
|
5124
|
+
if (/\bvariant\s*=/.test(props)) continue;
|
|
5125
|
+
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5126
|
+
const line = code.slice(0, match.index).split("\n").length;
|
|
5127
|
+
issues.push({
|
|
5128
|
+
line,
|
|
5129
|
+
type: "BUTTON_MISSING_VARIANT",
|
|
5130
|
+
message: '<Button> with navigation-style classes (text-muted-foreground) but no variant \u2014 add variant="ghost"',
|
|
5131
|
+
severity: "error"
|
|
5132
|
+
});
|
|
5133
|
+
}
|
|
5134
|
+
return issues;
|
|
5135
|
+
},
|
|
5136
|
+
fix(code) {
|
|
5137
|
+
let result = code;
|
|
5138
|
+
let applied = false;
|
|
5139
|
+
const buttonRe = /<Button\s/g;
|
|
5140
|
+
let match;
|
|
5141
|
+
let offset = 0;
|
|
5142
|
+
while ((match = buttonRe.exec(code)) !== null) {
|
|
5143
|
+
const adjustedIndex = match.index + offset;
|
|
5144
|
+
const props = extractJsxElementProps(result, adjustedIndex);
|
|
5145
|
+
if (!props) continue;
|
|
5146
|
+
if (/\bvariant\s*=/.test(props)) continue;
|
|
5147
|
+
if (!NAV_STYLE_SIGNAL.test(props)) continue;
|
|
5148
|
+
const insertPos = adjustedIndex + "<Button".length;
|
|
5149
|
+
const insertion = "\n" + getIndent(result, adjustedIndex) + ' variant="ghost"';
|
|
5150
|
+
result = result.slice(0, insertPos) + insertion + result.slice(insertPos);
|
|
5151
|
+
offset += insertion.length;
|
|
5152
|
+
applied = true;
|
|
5153
|
+
}
|
|
5154
|
+
return {
|
|
5155
|
+
code: result,
|
|
5156
|
+
applied,
|
|
5157
|
+
description: 'added variant="ghost" to Button with nav-style classes'
|
|
5158
|
+
};
|
|
5159
|
+
}
|
|
5160
|
+
};
|
|
5161
|
+
function getIndent(code, pos) {
|
|
5162
|
+
const lineStart = code.lastIndexOf("\n", pos);
|
|
5163
|
+
const lineContent = code.slice(lineStart + 1, pos);
|
|
5164
|
+
const indentMatch = lineContent.match(/^(\s*)/);
|
|
5165
|
+
return indentMatch ? indentMatch[1] : "";
|
|
5166
|
+
}
|
|
5167
|
+
var rules = [buttonMissingGhostVariant];
|
|
5168
|
+
function detectComponentIssues(code) {
|
|
5169
|
+
const issues = [];
|
|
5170
|
+
for (const rule of rules) {
|
|
5171
|
+
issues.push(...rule.detect(code));
|
|
5172
|
+
}
|
|
5173
|
+
return issues;
|
|
5174
|
+
}
|
|
5175
|
+
function applyComponentRules(code) {
|
|
5176
|
+
const fixes = [];
|
|
5177
|
+
let result = code;
|
|
5178
|
+
for (const rule of rules) {
|
|
5179
|
+
const { code: fixed, applied, description } = rule.fix(result);
|
|
5180
|
+
if (applied) {
|
|
5181
|
+
result = fixed;
|
|
5182
|
+
fixes.push(description);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
return { code: result, fixes };
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5059
5188
|
// src/utils/quality-validator.ts
|
|
5060
5189
|
var RAW_COLOR_RE = /(?:(?:hover|focus|active|group-hover|focus-visible|focus-within):)?(?:bg|text|border|ring|outline|from|to|via)-(gray|blue|red|green|yellow|purple|pink|indigo|orange|slate|zinc|stone|neutral|emerald|teal|cyan|sky|violet|fuchsia|rose|amber|lime)-\d+/g;
|
|
5061
5190
|
var HEX_IN_CLASS_RE = /className="[^"]*#[0-9a-fA-F]{3,8}[^"]*"/g;
|
|
@@ -5478,6 +5607,7 @@ function validatePageQuality(code, validRoutes) {
|
|
|
5478
5607
|
severity: "error"
|
|
5479
5608
|
});
|
|
5480
5609
|
}
|
|
5610
|
+
issues.push(...detectComponentIssues(code));
|
|
5481
5611
|
return issues;
|
|
5482
5612
|
}
|
|
5483
5613
|
function replaceRawColors(classes, colorMap) {
|
|
@@ -5576,6 +5706,7 @@ async function autoFixCode(code) {
|
|
|
5576
5706
|
const fixes = [];
|
|
5577
5707
|
let fixed = code;
|
|
5578
5708
|
const beforeQuoteFix = fixed;
|
|
5709
|
+
fixed = fixed.replace(/\\'(\s*[}\],])/g, "'$1");
|
|
5579
5710
|
fixed = fixed.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
|
|
5580
5711
|
if (fixed !== beforeQuoteFix) {
|
|
5581
5712
|
fixes.push("fixed escaped closing quotes in strings");
|
|
@@ -5886,6 +6017,23 @@ ${selectImport}`
|
|
|
5886
6017
|
}
|
|
5887
6018
|
}
|
|
5888
6019
|
}
|
|
6020
|
+
const lucideNamesMatch = fixed.match(/import\s*\{([^}]+)\}\s*from\s*["']lucide-react["']/);
|
|
6021
|
+
if (lucideNamesMatch) {
|
|
6022
|
+
const lucideNames = new Set(
|
|
6023
|
+
lucideNamesMatch[1].split(",").map((s) => s.trim()).filter(Boolean)
|
|
6024
|
+
);
|
|
6025
|
+
const beforeShrinkFix = fixed;
|
|
6026
|
+
for (const iconName of lucideNames) {
|
|
6027
|
+
const iconRe = new RegExp(`(<${iconName}\\s[^>]*className=")([^"]*)(")`, "g");
|
|
6028
|
+
fixed = fixed.replace(iconRe, (_m, pre, classes, post) => {
|
|
6029
|
+
if (/\bshrink-0\b/.test(classes)) return _m;
|
|
6030
|
+
return `${pre}${classes} shrink-0${post}`;
|
|
6031
|
+
});
|
|
6032
|
+
}
|
|
6033
|
+
if (fixed !== beforeShrinkFix) {
|
|
6034
|
+
fixes.push("added shrink-0 to icons");
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
5889
6037
|
const linkWithButtonRe = /(<Link\b[^>]*>)\s*(<Button\b(?![^>]*asChild)[^>]*>)([\s\S]*?)<\/Button>\s*<\/Link>/g;
|
|
5890
6038
|
const beforeLinkFix = fixed;
|
|
5891
6039
|
fixed = fixed.replace(linkWithButtonRe, (_match, linkOpen, buttonOpen, inner) => {
|
|
@@ -5897,6 +6045,11 @@ ${selectImport}`
|
|
|
5897
6045
|
if (fixed !== beforeLinkFix) {
|
|
5898
6046
|
fixes.push("Link>Button \u2192 Button asChild>Link (DOM nesting fix)");
|
|
5899
6047
|
}
|
|
6048
|
+
const { code: fixedByRules, fixes: ruleFixes } = applyComponentRules(fixed);
|
|
6049
|
+
if (ruleFixes.length > 0) {
|
|
6050
|
+
fixed = fixedByRules;
|
|
6051
|
+
fixes.push(...ruleFixes);
|
|
6052
|
+
}
|
|
5900
6053
|
fixed = fixed.replace(/className="([^"]*)"/g, (_match, inner) => {
|
|
5901
6054
|
const cleaned = inner.replace(/\s{2,}/g, " ").trim();
|
|
5902
6055
|
return `className="${cleaned}"`;
|
|
@@ -6779,6 +6932,10 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6779
6932
|
} catch {
|
|
6780
6933
|
spinner.text = "AI plan failed \u2014 extracting pages from your request...";
|
|
6781
6934
|
}
|
|
6935
|
+
if (modCtx.config.name === "My App") {
|
|
6936
|
+
const nameFromPrompt = extractAppNameFromPrompt(message);
|
|
6937
|
+
if (nameFromPrompt) modCtx.config.name = nameFromPrompt;
|
|
6938
|
+
}
|
|
6782
6939
|
if (pageNames.length === 0) {
|
|
6783
6940
|
pageNames = extractPageNamesFromMessage(message);
|
|
6784
6941
|
}
|
|
@@ -6908,6 +7065,45 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
|
|
|
6908
7065
|
spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
|
|
6909
7066
|
return allRequests;
|
|
6910
7067
|
}
|
|
7068
|
+
function extractAppNameFromPrompt(prompt) {
|
|
7069
|
+
const patterns = [
|
|
7070
|
+
/(?:called|named|app\s+name)\s+["']([^"']+)["']/i,
|
|
7071
|
+
/(?:called|named|app\s+name)\s+(\S+)/i,
|
|
7072
|
+
/\b(?:build|create|make)\s+(?:a\s+)?(\S+)\s+(?:app|platform|tool|dashboard|website|saas)/i
|
|
7073
|
+
];
|
|
7074
|
+
for (const re of patterns) {
|
|
7075
|
+
const m = prompt.match(re);
|
|
7076
|
+
if (m && m[1] && m[1].length >= 2 && m[1].length <= 30) {
|
|
7077
|
+
const name = m[1].replace(/[.,;:!?]$/, "");
|
|
7078
|
+
const skip = /* @__PURE__ */ new Set([
|
|
7079
|
+
"a",
|
|
7080
|
+
"an",
|
|
7081
|
+
"the",
|
|
7082
|
+
"my",
|
|
7083
|
+
"our",
|
|
7084
|
+
"new",
|
|
7085
|
+
"full",
|
|
7086
|
+
"complete",
|
|
7087
|
+
"simple",
|
|
7088
|
+
"modern",
|
|
7089
|
+
"beautiful",
|
|
7090
|
+
"responsive",
|
|
7091
|
+
"fast",
|
|
7092
|
+
"cool",
|
|
7093
|
+
"great",
|
|
7094
|
+
"basic",
|
|
7095
|
+
"quick",
|
|
7096
|
+
"small",
|
|
7097
|
+
"large",
|
|
7098
|
+
"custom",
|
|
7099
|
+
"nice"
|
|
7100
|
+
]);
|
|
7101
|
+
if (skip.has(name.toLowerCase())) continue;
|
|
7102
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
return null;
|
|
7106
|
+
}
|
|
6911
7107
|
|
|
6912
7108
|
// src/commands/chat/modification-handler.ts
|
|
6913
7109
|
import { resolve as resolve7 } from "path";
|
|
@@ -7832,19 +8028,24 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7832
8028
|
if (aiPageCode) {
|
|
7833
8029
|
finalPageCode = aiPageCode;
|
|
7834
8030
|
if (DEBUG2) console.log(chalk11.dim(` [pageCode] Using AI-generated pageCode (user content priority)`));
|
|
7835
|
-
} else
|
|
7836
|
-
const
|
|
7837
|
-
if (
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
8031
|
+
} else {
|
|
8032
|
+
const inferredType = page.pageType || inferPageType(page.route || "", page.name || "");
|
|
8033
|
+
if (inferredType) {
|
|
8034
|
+
const templateFn = getTemplateForPageType(inferredType);
|
|
8035
|
+
if (templateFn) {
|
|
8036
|
+
try {
|
|
8037
|
+
const pageName = (page.name || "Page").replace(/\s+/g, "");
|
|
8038
|
+
const opts = {
|
|
8039
|
+
route: page.route || `/${page.id || "page"}`,
|
|
8040
|
+
pageName
|
|
8041
|
+
};
|
|
8042
|
+
const content = page.structuredContent || getDefaultContent(inferredType, page.name || pageName);
|
|
8043
|
+
finalPageCode = templateFn(content, opts);
|
|
8044
|
+
if (DEBUG2)
|
|
8045
|
+
console.log(chalk11.dim(` [template] Used "${inferredType}" template (inferred from route/name)`));
|
|
8046
|
+
} catch {
|
|
8047
|
+
if (DEBUG2) console.log(chalk11.dim(` [template] Failed for "${inferredType}"`));
|
|
8048
|
+
}
|
|
7848
8049
|
}
|
|
7849
8050
|
}
|
|
7850
8051
|
}
|
|
@@ -8216,6 +8417,65 @@ ${pagesCtx}`
|
|
|
8216
8417
|
};
|
|
8217
8418
|
}
|
|
8218
8419
|
}
|
|
8420
|
+
function inferPageType(route, name) {
|
|
8421
|
+
const key = (route + " " + name).toLowerCase();
|
|
8422
|
+
if (/\blogin\b|\bsign.?in\b/.test(key)) return "login";
|
|
8423
|
+
if (/\bregister\b|\bsign.?up\b/.test(key)) return "register";
|
|
8424
|
+
if (/\bdashboard\b/.test(key)) return "dashboard";
|
|
8425
|
+
if (/\bpric(e|ing)\b/.test(key)) return "pricing";
|
|
8426
|
+
if (/\bfaq\b/.test(key)) return "faq";
|
|
8427
|
+
if (/\bcontact\b/.test(key)) return "contact";
|
|
8428
|
+
if (/\bblog\b/.test(key)) return "blog";
|
|
8429
|
+
if (/\bchangelog\b/.test(key)) return "changelog";
|
|
8430
|
+
if (/\babout\b/.test(key)) return "about";
|
|
8431
|
+
if (/\bsettings?\b/.test(key)) return "settings";
|
|
8432
|
+
return null;
|
|
8433
|
+
}
|
|
8434
|
+
function getDefaultContent(pageType, pageName) {
|
|
8435
|
+
const defaults = {
|
|
8436
|
+
login: {
|
|
8437
|
+
title: "Welcome back",
|
|
8438
|
+
description: "Sign in to your account to continue"
|
|
8439
|
+
},
|
|
8440
|
+
register: {
|
|
8441
|
+
title: "Create an account",
|
|
8442
|
+
description: "Get started with your free account"
|
|
8443
|
+
},
|
|
8444
|
+
dashboard: {
|
|
8445
|
+
title: "Dashboard",
|
|
8446
|
+
description: `Welcome to your ${pageName} dashboard`
|
|
8447
|
+
},
|
|
8448
|
+
pricing: {
|
|
8449
|
+
title: "Pricing",
|
|
8450
|
+
description: "Choose the plan that works for you"
|
|
8451
|
+
},
|
|
8452
|
+
faq: {
|
|
8453
|
+
title: "Frequently Asked Questions",
|
|
8454
|
+
description: "Find answers to common questions"
|
|
8455
|
+
},
|
|
8456
|
+
contact: {
|
|
8457
|
+
title: "Contact Us",
|
|
8458
|
+
description: "Get in touch with our team"
|
|
8459
|
+
},
|
|
8460
|
+
blog: {
|
|
8461
|
+
title: "Blog",
|
|
8462
|
+
description: "Latest news, updates, and insights"
|
|
8463
|
+
},
|
|
8464
|
+
changelog: {
|
|
8465
|
+
title: "Changelog",
|
|
8466
|
+
description: "What's new and improved"
|
|
8467
|
+
},
|
|
8468
|
+
about: {
|
|
8469
|
+
title: `About ${pageName}`,
|
|
8470
|
+
description: "Learn more about our mission and team"
|
|
8471
|
+
},
|
|
8472
|
+
settings: {
|
|
8473
|
+
title: "Settings",
|
|
8474
|
+
description: "Manage your account and preferences"
|
|
8475
|
+
}
|
|
8476
|
+
};
|
|
8477
|
+
return defaults[pageType] || { title: pageName, description: "" };
|
|
8478
|
+
}
|
|
8219
8479
|
|
|
8220
8480
|
// src/utils/nav-snapshot.ts
|
|
8221
8481
|
function takeNavSnapshot(items) {
|