@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.
Files changed (2) hide show
  1. package/dist/index.js +275 -15
  2. 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(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
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
- return code.replace(/(:\s*'.+)\\'(\s*)$/gm, "$1'$2");
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 if (page.pageType && page.structuredContent) {
7836
- const templateFn = getTemplateForPageType(page.pageType);
7837
- if (templateFn) {
7838
- try {
7839
- const pageName = (page.name || "Page").replace(/\s+/g, "");
7840
- const opts = {
7841
- route: page.route || `/${page.id || "page"}`,
7842
- pageName
7843
- };
7844
- finalPageCode = templateFn(page.structuredContent, opts);
7845
- if (DEBUG2) console.log(chalk11.dim(` [template] Used "${page.pageType}" template (no pageCode provided)`));
7846
- } catch {
7847
- if (DEBUG2) console.log(chalk11.dim(` [template] Failed for "${page.pageType}"`));
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) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.7",
6
+ "version": "0.5.9",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",