@getcoherent/cli 0.6.22 → 0.6.24

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.
@@ -703,17 +703,9 @@ async function autoFixCode(code, context) {
703
703
  };
704
704
  fixed = fixed.split("\n").map((line) => {
705
705
  let l = line;
706
- l = l.replace(/&lt;=/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "<=");
707
- l = l.replace(/&gt;=/g, (m, offset) => isInsideAttrValue(line, offset) ? m : ">=");
708
- l = l.replace(/&amp;&amp;/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "&&");
709
- l = l.replace(
710
- /([\w)\]])\s*&lt;\s*([\w(])/g,
711
- (m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} < ${p2}`
712
- );
713
- l = l.replace(
714
- /([\w)\]])\s*&gt;\s*([\w(])/g,
715
- (m, p1, p2, offset) => isInsideAttrValue(line, offset) ? m : `${p1} > ${p2}`
716
- );
706
+ l = l.replace(/&lt;/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "<");
707
+ l = l.replace(/&gt;/g, (m, offset) => isInsideAttrValue(line, offset) ? m : ">");
708
+ l = l.replace(/&amp;/g, (m, offset) => isInsideAttrValue(line, offset) ? m : "&");
717
709
  return l;
718
710
  }).join("\n");
719
711
  if (fixed !== beforeEntityFix) {
@@ -807,6 +799,97 @@ ${fixed}`;
807
799
  }
808
800
  fixes.push("<button> \u2192 <Button> (with import)");
809
801
  }
802
+ const compositeComponents = {
803
+ select: [
804
+ "Select",
805
+ "SelectContent",
806
+ "SelectItem",
807
+ "SelectTrigger",
808
+ "SelectValue",
809
+ "SelectGroup",
810
+ "SelectLabel",
811
+ "SelectSeparator",
812
+ "SelectScrollUpButton",
813
+ "SelectScrollDownButton"
814
+ ],
815
+ dialog: [
816
+ "Dialog",
817
+ "DialogContent",
818
+ "DialogDescription",
819
+ "DialogFooter",
820
+ "DialogHeader",
821
+ "DialogTitle",
822
+ "DialogTrigger",
823
+ "DialogClose",
824
+ "DialogOverlay",
825
+ "DialogPortal"
826
+ ],
827
+ dropdown_menu: [
828
+ "DropdownMenu",
829
+ "DropdownMenuContent",
830
+ "DropdownMenuItem",
831
+ "DropdownMenuLabel",
832
+ "DropdownMenuSeparator",
833
+ "DropdownMenuTrigger",
834
+ "DropdownMenuCheckboxItem",
835
+ "DropdownMenuGroup",
836
+ "DropdownMenuRadioGroup",
837
+ "DropdownMenuRadioItem",
838
+ "DropdownMenuShortcut",
839
+ "DropdownMenuSub",
840
+ "DropdownMenuSubContent",
841
+ "DropdownMenuSubTrigger"
842
+ ],
843
+ table: ["Table", "TableBody", "TableCaption", "TableCell", "TableFooter", "TableHead", "TableHeader", "TableRow"],
844
+ tabs: ["Tabs", "TabsContent", "TabsList", "TabsTrigger"],
845
+ card: ["Card", "CardContent", "CardDescription", "CardFooter", "CardHeader", "CardTitle"],
846
+ alert_dialog: [
847
+ "AlertDialog",
848
+ "AlertDialogAction",
849
+ "AlertDialogCancel",
850
+ "AlertDialogContent",
851
+ "AlertDialogDescription",
852
+ "AlertDialogFooter",
853
+ "AlertDialogHeader",
854
+ "AlertDialogTitle",
855
+ "AlertDialogTrigger"
856
+ ],
857
+ popover: ["Popover", "PopoverContent", "PopoverTrigger"],
858
+ command: [
859
+ "Command",
860
+ "CommandDialog",
861
+ "CommandEmpty",
862
+ "CommandGroup",
863
+ "CommandInput",
864
+ "CommandItem",
865
+ "CommandList",
866
+ "CommandSeparator",
867
+ "CommandShortcut"
868
+ ],
869
+ form: ["Form", "FormControl", "FormDescription", "FormField", "FormItem", "FormLabel", "FormMessage"]
870
+ };
871
+ const beforeSubImportFix = fixed;
872
+ for (const [uiName, allExports] of Object.entries(compositeComponents)) {
873
+ const importPath = `@/components/ui/${uiName.replace(/_/g, "-")}`;
874
+ const importRe = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${importPath.replace(/[-/]/g, "\\$&")}['"]`);
875
+ const importMatch = fixed.match(importRe);
876
+ if (!importMatch) continue;
877
+ const imported = new Set(
878
+ importMatch[1].split(",").map((s) => s.trim()).filter(Boolean)
879
+ );
880
+ const usedInCode = allExports.filter((e) => {
881
+ if (imported.has(e)) return false;
882
+ return new RegExp(`<${e}[\\s/>]`).test(fixed) || new RegExp(`</${e}>`).test(fixed);
883
+ });
884
+ if (usedInCode.length > 0) {
885
+ const merged = [...imported, ...usedInCode];
886
+ const newImport = `import { ${merged.join(", ")} } from '${importPath}'`;
887
+ fixed = fixed.replace(importRe, newImport);
888
+ }
889
+ }
890
+ if (fixed !== beforeSubImportFix) {
891
+ fixes.push("added missing sub-imports for composite components");
892
+ }
810
893
  const colorMap = {
811
894
  "bg-zinc-950": "bg-background",
812
895
  "bg-zinc-900": "bg-background",
@@ -1186,6 +1269,11 @@ ${selectImport}`
1186
1269
  if (fixed !== beforePlaceholder) {
1187
1270
  fixes.push("placeholder content \u2192 contextual content");
1188
1271
  }
1272
+ const beforeIconProp = fixed;
1273
+ fixed = fixed.replace(/(\bicon\s*:\s*)React\.ReactNode\b/g, "$1React.ElementType");
1274
+ if (fixed !== beforeIconProp) {
1275
+ fixes.push("icon prop: ReactNode \u2192 ElementType (forwardRef compat)");
1276
+ }
1189
1277
  return { code: fixed, fixes };
1190
1278
  }
1191
1279
  function formatIssues(issues) {
@@ -236,6 +236,29 @@ function loadPlan(projectRoot) {
236
236
  function toKebabCase(name) {
237
237
  return name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
238
238
  }
239
+ function extractPropsInterface(code, componentName) {
240
+ const interfaceRe = new RegExp(`interface\\s+${componentName}Props\\s*\\{([^}]+)\\}`, "s");
241
+ const match = code.match(interfaceRe);
242
+ if (match) {
243
+ return match[1].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//")).join("; ");
244
+ }
245
+ const typeRe = new RegExp(`type\\s+${componentName}Props\\s*=\\s*\\{([^}]+)\\}`, "s");
246
+ const typeMatch = code.match(typeRe);
247
+ if (typeMatch) {
248
+ return typeMatch[1].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//")).join("; ");
249
+ }
250
+ return void 0;
251
+ }
252
+ function extractUsageExample(code, componentName) {
253
+ const funcMatch = code.match(new RegExp(`export function ${componentName}\\s*\\(\\{([^}]+)\\}`, "s"));
254
+ if (!funcMatch) return void 0;
255
+ const props = funcMatch[1].split(",").map((p) => p.split(":")[0].trim()).filter(Boolean);
256
+ const example = props.map((p) => {
257
+ if (p.startsWith("...")) return `${p.slice(3)}={{}}`;
258
+ return `${p}={...}`;
259
+ }).join(" ");
260
+ return `<${componentName} ${example} />`;
261
+ }
239
262
  async function generateSharedComponentsFromPlan(plan, styleContext, projectRoot, aiProvider) {
240
263
  if (plan.sharedComponents.length === 0) return [];
241
264
  const componentSpecs = plan.sharedComponents.map(
@@ -258,6 +281,7 @@ Requirements:
258
281
  - Use Tailwind CSS classes matching the style context
259
282
  - TypeScript with proper props interface
260
283
  - Each component is a standalone file
284
+ - Icon props MUST use \`icon: React.ElementType\` (NOT React.ReactNode) and render as \`<Icon className="size-4" />\` where \`const Icon = icon\`. Lucide icons are forwardRef components, not elements.
261
285
 
262
286
  Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentName", pageCode: "..." } }, ...] }`;
263
287
  const results = [];
@@ -296,6 +320,8 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
296
320
  }
297
321
  for (const comp of results) {
298
322
  const planned = plan.sharedComponents.find((c) => c.name === comp.name);
323
+ const propsInterface = extractPropsInterface(comp.code, comp.name);
324
+ const usageExample = extractUsageExample(comp.code, comp.name);
299
325
  await generateSharedComponent(projectRoot, {
300
326
  name: comp.name,
301
327
  type: planned?.type ?? "section",
@@ -303,7 +329,9 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
303
329
  description: planned?.description,
304
330
  usedIn: planned?.usedBy ?? [],
305
331
  source: "generated",
306
- overwrite: true
332
+ overwrite: true,
333
+ propsInterface,
334
+ usageExample
307
335
  });
308
336
  }
309
337
  return results;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  formatIssues,
8
8
  validatePageQuality,
9
9
  verifyIncrementalEdit
10
- } from "./chunk-VKGZRKWD.js";
10
+ } from "./chunk-2ZL4X4QD.js";
11
11
  import {
12
12
  generateArchitecturePlan,
13
13
  getPageGroup,
@@ -16,7 +16,7 @@ import {
16
16
  routeToKey,
17
17
  savePlan,
18
18
  updateArchitecturePlan
19
- } from "./chunk-PVJJ2YXP.js";
19
+ } from "./chunk-IYLHC4RC.js";
20
20
  import {
21
21
  CORE_CONSTRAINTS,
22
22
  DESIGN_QUALITY,
@@ -4393,6 +4393,12 @@ PAGE WRAPPER (CRITICAL \u2014 the layout provides width/padding automatically):
4393
4393
  - ALL app pages must follow this exact same structure so content aligns consistently across pages
4394
4394
  - Landing/marketing pages are an exception: they render outside the app layout and should use full-width <section> elements with inner "mx-auto max-w-6xl" for content.
4395
4395
 
4396
+ ICON PROPS (CRITICAL \u2014 prevents runtime crash):
4397
+ - When passing lucide-react icons as props, the component prop MUST be typed as \`React.ElementType\` (NOT React.ReactNode).
4398
+ - Render icon props as: \`const Icon = props.icon; <Icon className="size-4" />\`
4399
+ - Pass icons as: \`icon={FolderOpen}\` (component reference, not JSX element)
4400
+ - Lucide icons are forwardRef components \u2014 passing them as ReactNode causes "Objects are not valid as a React child" error.
4401
+
4396
4402
  PAGE CONTENT (CRITICAL \u2014 prevents empty or duplicate pages):
4397
4403
  - Every page MUST have substantial content. NEVER generate a page with only metadata and an empty <main> element.
4398
4404
  - NEVER create an inline preview/demo of another page (e.g., embedding a "dashboard view" inside the landing page with a toggle). Each page should be its own route.
@@ -5096,7 +5102,7 @@ async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, mani
5096
5102
  if (pageTokens.includes(t)) overlap++;
5097
5103
  }
5098
5104
  const overlapRatio = sharedTokens.size > 0 ? overlap / sharedTokens.size : 0;
5099
- if (overlap >= 20 && overlapRatio >= 0.6) {
5105
+ if (overlap >= 25 && overlapRatio >= 0.7) {
5100
5106
  console.log(
5101
5107
  chalk7.yellow(
5102
5108
  `
@@ -6309,7 +6315,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
6309
6315
  if (plan && plan.sharedComponents.length > 0) {
6310
6316
  spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
6311
6317
  try {
6312
- const { generateSharedComponentsFromPlan } = await import("./plan-generator-ITHYNYJI.js");
6318
+ const { generateSharedComponentsFromPlan } = await import("./plan-generator-BHDEJGMY.js");
6313
6319
  const generated = await generateSharedComponentsFromPlan(
6314
6320
  plan,
6315
6321
  styleContext,
@@ -6769,7 +6775,7 @@ async function regeneratePage(pageId, config2, projectRoot) {
6769
6775
  const code = await generator.generate(page, appType);
6770
6776
  const route = page.route || "/";
6771
6777
  const isAuth = isAuthRoute(route) || isAuthRoute(page.name || page.id || "");
6772
- const { loadPlan: loadPlanForPath } = await import("./plan-generator-ITHYNYJI.js");
6778
+ const { loadPlan: loadPlanForPath } = await import("./plan-generator-BHDEJGMY.js");
6773
6779
  const planForPath = loadPlanForPath(projectRoot);
6774
6780
  const filePath = routeToFsPath(projectRoot, route, planForPath || isAuth);
6775
6781
  await mkdir3(dirname5(filePath), { recursive: true });
@@ -7085,7 +7091,17 @@ function extractImportsFrom(code, fromPath) {
7085
7091
  return [...new Set(results)];
7086
7092
  }
7087
7093
  function printPostGenerationReport(opts) {
7088
- const { action, pageTitle, filePath, code, route, postFixes = [], layoutShared = [], allShared = [] } = opts;
7094
+ const {
7095
+ action,
7096
+ pageTitle,
7097
+ filePath,
7098
+ code,
7099
+ route,
7100
+ postFixes = [],
7101
+ layoutShared = [],
7102
+ allShared = [],
7103
+ groupLayout
7104
+ } = opts;
7089
7105
  const uiComponents = extractImportsFrom(code, "@/components/ui");
7090
7106
  const sharedImportNames = extractImportsFrom(code, "@/components/shared/");
7091
7107
  const inCodeShared = allShared.filter((s) => sharedImportNames.some((n) => n === s.name));
@@ -7102,8 +7118,10 @@ function printPostGenerationReport(opts) {
7102
7118
  console.log(chalk10.dim(` Shared: ${inCodeShared.map((s) => `${s.id} (${s.name})`).join(", ")}`));
7103
7119
  }
7104
7120
  const isAuthPage = route && (/^\/(login|signin|signup|register|forgot-password|reset-password)\b/.test(route) || filePath.includes("(auth)"));
7105
- if (layoutShared.length > 0 && !isAuthPage) {
7106
- console.log(chalk10.dim(` Layout: ${layoutShared.map((l) => `${l.id} (${l.name})`).join(", ")} via layout.tsx`));
7121
+ const isSidebarPage = groupLayout === "sidebar" || filePath.includes("(app)");
7122
+ const filteredLayout = isAuthPage ? [] : isSidebarPage ? layoutShared.filter((l) => /sidebar/i.test(l.name)) : layoutShared;
7123
+ if (filteredLayout.length > 0) {
7124
+ console.log(chalk10.dim(` Layout: ${filteredLayout.map((l) => `${l.id} (${l.name})`).join(", ")} via layout.tsx`));
7107
7125
  }
7108
7126
  if (iconCount > 0) {
7109
7127
  console.log(chalk10.dim(` Icons: ${iconCount} from lucide-react`));
@@ -7512,7 +7530,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7512
7530
  sharedId: resolved.id,
7513
7531
  sharedName: resolved.name,
7514
7532
  pageTarget,
7515
- route: route ?? `/${routePath}`,
7533
+ route,
7516
7534
  postFixes: fixes
7517
7535
  });
7518
7536
  try {
@@ -8528,7 +8546,7 @@ async function chatCommand(message, options) {
8528
8546
  spinner.start(`Creating shared component: ${componentName}...`);
8529
8547
  const { createAIProvider: createAIProvider2 } = await import("./ai-provider-CGSIYFZT.js");
8530
8548
  const { generateSharedComponent: generateSharedComponent7 } = await import("@getcoherent/core");
8531
- const { autoFixCode: autoFixCode2 } = await import("./quality-validator-P572ZUW5.js");
8549
+ const { autoFixCode: autoFixCode2 } = await import("./quality-validator-G5AE4337.js");
8532
8550
  const { extractPropsInterface, extractDependencies } = await import("./component-extractor-VYJLT5NR.js");
8533
8551
  const aiProvider = await createAIProvider2(provider ?? "auto");
8534
8552
  const prompt = `Generate a React component called "${componentName}". Description: ${message}.
@@ -11,7 +11,7 @@ import {
11
11
  routeToKey,
12
12
  savePlan,
13
13
  updateArchitecturePlan
14
- } from "./chunk-PVJJ2YXP.js";
14
+ } from "./chunk-IYLHC4RC.js";
15
15
  import "./chunk-5AHG4NNX.js";
16
16
  import "./chunk-3RG5ZIWI.js";
17
17
  export {
@@ -4,7 +4,7 @@ import {
4
4
  formatIssues,
5
5
  validatePageQuality,
6
6
  verifyIncrementalEdit
7
- } from "./chunk-VKGZRKWD.js";
7
+ } from "./chunk-2ZL4X4QD.js";
8
8
  import "./chunk-3RG5ZIWI.js";
9
9
  export {
10
10
  autoFixCode,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.22",
6
+ "version": "0.6.24",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -43,7 +43,7 @@
43
43
  "ora": "^7.0.1",
44
44
  "prompts": "^2.4.2",
45
45
  "zod": "^3.22.4",
46
- "@getcoherent/core": "0.6.22"
46
+ "@getcoherent/core": "0.6.24"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^20.11.0",