@getcoherent/cli 0.6.14 → 0.6.15

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.
@@ -3,7 +3,7 @@ import {
3
3
  detectAIProvider,
4
4
  getAPIKey,
5
5
  hasAnyAPIKey
6
- } from "./chunk-25JRF5MA.js";
6
+ } from "./chunk-SPQZBQYY.js";
7
7
  import "./chunk-3RG5ZIWI.js";
8
8
  export {
9
9
  createAIProvider,
@@ -253,7 +253,7 @@ Style context: ${styleContext || "default"}
253
253
  ${designRules}
254
254
 
255
255
  Requirements:
256
- - Each component MUST have \`export default function ComponentName\`
256
+ - Each component MUST use a NAMED export: \`export function ComponentName\` (NOT export default)
257
257
  - Use shadcn/ui imports from @/components/ui/*
258
258
  - Use Tailwind CSS classes matching the style context
259
259
  - TypeScript with proper props interface
@@ -269,9 +269,10 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
269
269
  (r) => r.type === "add-page" && r.changes?.name === comp.name
270
270
  );
271
271
  const code = match?.changes?.pageCode;
272
- if (code && code.includes("export default")) {
272
+ if (code && (code.includes("export function") || code.includes("export default"))) {
273
+ const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
273
274
  const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
274
- results.push({ name: comp.name, code, file });
275
+ results.push({ name: comp.name, code: fixedCode, file });
275
276
  }
276
277
  }
277
278
  } catch {
@@ -284,9 +285,10 @@ Return JSON with { requests: [{ type: "add-page", changes: { name: "ComponentNam
284
285
  (r) => r.type === "add-page" && r.changes?.name === comp.name
285
286
  );
286
287
  const code = match?.changes?.pageCode;
287
- if (code && code.includes("export default")) {
288
+ if (code && (code.includes("export function") || code.includes("export default"))) {
289
+ const fixedCode = code.replace(/export default function (\w+)/g, "export function $1");
288
290
  const file = `components/shared/${toKebabCase(comp.name)}.tsx`;
289
- results.push({ name: comp.name, code, file });
291
+ results.push({ name: comp.name, code: fixedCode, file });
290
292
  }
291
293
  } catch {
292
294
  }
@@ -68,7 +68,7 @@ Please set ${envVar} in your environment or .env file.`);
68
68
  }
69
69
  if (preferredProvider === "openai") {
70
70
  try {
71
- const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
71
+ const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
72
72
  return await OpenAIClient.create(apiKey2, config?.model);
73
73
  } catch (error) {
74
74
  if (error.message?.includes("not installed")) {
@@ -82,7 +82,7 @@ Error: ${error.message}`
82
82
  );
83
83
  }
84
84
  } else {
85
- const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
85
+ const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
86
86
  return ClaudeClient.create(apiKey2, config?.model);
87
87
  }
88
88
  }
@@ -95,7 +95,7 @@ Error: ${error.message}`
95
95
  switch (provider) {
96
96
  case "openai":
97
97
  try {
98
- const { OpenAIClient } = await import("./openai-provider-XUI7ZHUR.js");
98
+ const { OpenAIClient } = await import("./openai-provider-4KGARVC3.js");
99
99
  return await OpenAIClient.create(apiKey, config?.model);
100
100
  } catch (error) {
101
101
  if (error.message?.includes("not installed")) {
@@ -109,7 +109,7 @@ Error: ${error.message}`
109
109
  );
110
110
  }
111
111
  case "claude":
112
- const { ClaudeClient } = await import("./claude-BZ3HSBD3.js");
112
+ const { ClaudeClient } = await import("./claude-K5B5ITAT.js");
113
113
  return ClaudeClient.create(apiKey, config?.model);
114
114
  default:
115
115
  throw new Error(`Unsupported AI provider: ${provider}`);
@@ -399,7 +399,7 @@ Rules:
399
399
  - Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
400
400
  - Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
401
401
 
402
- Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
402
+ Each component object: "name" (PascalCase), "type" ("layout"|"navigation"|"data-display"|"form"|"feedback"|"section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
403
403
 
404
404
  If no repeating patterns found: { "components": [] }`
405
405
  }
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createAIProvider
3
- } from "./chunk-25JRF5MA.js";
3
+ } from "./chunk-SPQZBQYY.js";
4
4
  import {
5
5
  autoFixCode,
6
6
  checkDesignConsistency,
@@ -14,8 +14,9 @@ import {
14
14
  getPageType,
15
15
  loadPlan,
16
16
  routeToKey,
17
- savePlan
18
- } from "./chunk-WRDWFCQJ.js";
17
+ savePlan,
18
+ updateArchitecturePlan
19
+ } from "./chunk-PVJJ2YXP.js";
19
20
  import {
20
21
  CORE_CONSTRAINTS,
21
22
  DESIGN_QUALITY,
@@ -2464,7 +2465,7 @@ import { loadManifest } from "@getcoherent/core";
2464
2465
  import { DesignSystemManager } from "@getcoherent/core";
2465
2466
  function buildSharedComponentsListForClaude(manifest) {
2466
2467
  if (!manifest.shared || manifest.shared.length === 0) {
2467
- return "No shared components yet. Register with: coherent components shared add <name> --type layout|section|widget";
2468
+ return "No shared components yet. Register with: coherent components shared add <name> --type layout|navigation|data-display|form|feedback|section|widget";
2468
2469
  }
2469
2470
  const order = {
2470
2471
  layout: 0,
@@ -2959,7 +2960,7 @@ function buildSharedComponentsList(manifest) {
2959
2960
  if (!manifest.shared || manifest.shared.length === 0) {
2960
2961
  return `No shared components registered yet.
2961
2962
  When you create reusable blocks (headers, footers, repeated sections),
2962
- register them: coherent components shared add <Name> --type layout|section|widget`;
2963
+ register them: coherent components shared add <Name> --type layout|navigation|data-display|form|feedback|section|widget`;
2963
2964
  }
2964
2965
  const typeOrder = {
2965
2966
  layout: 0,
@@ -3857,7 +3858,7 @@ import {
3857
3858
  CLI_VERSION as CLI_VERSION2,
3858
3859
  getTemplateForPageType as getTemplateForPageType2,
3859
3860
  loadManifest as loadManifest8,
3860
- saveManifest as saveManifest2,
3861
+ saveManifest as saveManifest3,
3861
3862
  updateEntry
3862
3863
  } from "@getcoherent/core";
3863
3864
 
@@ -4101,7 +4102,7 @@ function detectPageType(pageName) {
4101
4102
  const normalized = pageName.toLowerCase();
4102
4103
  if (/dashboard|admin|overview/.test(normalized)) return "dashboard";
4103
4104
  if (/login|signin|sign-in/.test(normalized)) return "login";
4104
- if (/register|signup|sign.?up/.test(normalized)) return "register";
4105
+ if (/regist(?:er|ration)|signup|sign.?up/.test(normalized)) return "register";
4105
4106
  if (/pricing|plans|subscription/.test(normalized)) return "pricing";
4106
4107
  if (/about|company/.test(normalized)) return "about";
4107
4108
  if (/contact|support|help/.test(normalized)) return "contact";
@@ -4171,7 +4172,8 @@ async function parseModification(message, context, provider = "auto", options) {
4171
4172
  const prompt = buildModificationPrompt(enhancedMessage, context.config, componentRegistry, {
4172
4173
  isExpandedPageRequest,
4173
4174
  sharedComponentsSummary: options?.sharedComponentsSummary,
4174
- tieredComponentsPrompt: options?.tieredComponentsPrompt
4175
+ tieredComponentsPrompt: options?.tieredComponentsPrompt,
4176
+ reusePlanDirective: options?.reusePlanDirective
4175
4177
  });
4176
4178
  const raw = await ai.parseModification(prompt);
4177
4179
  const requestsArray = Array.isArray(raw) ? raw : raw?.requests ?? [];
@@ -4237,7 +4239,14 @@ Rules:
4237
4239
  function buildModificationPrompt(message, config2, componentRegistry, options) {
4238
4240
  const now = (/* @__PURE__ */ new Date()).toISOString();
4239
4241
  const expandedHint = options?.isExpandedPageRequest === true ? "\nIMPORTANT: The user request has been expanded with best practices. Use ALL the details provided when generating sections and content.\n\n" : "";
4240
- const sharedSection = options?.tieredComponentsPrompt ? `
4242
+ const sharedSection = options?.reusePlanDirective ? `
4243
+
4244
+ ## COMPONENT REUSE DIRECTIVE
4245
+
4246
+ ${options.reusePlanDirective}
4247
+
4248
+ For editing an existing shared component use type "modify-layout-block" with target "CID-XXX" or name.
4249
+ ` : options?.tieredComponentsPrompt ? `
4241
4250
 
4242
4251
  ## SHARED COMPONENTS (MANDATORY REUSE)
4243
4252
 
@@ -4520,9 +4529,9 @@ Return valid JSON only, no markdown code fence. Use this shape:
4520
4529
  { "requests": [ ... array of ModificationRequest ... ], "uxRecommendations": "optional markdown or omit key" }
4521
4530
  Legacy: returning only a JSON array of requests is still accepted.`;
4522
4531
  }
4523
- function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType, tieredComponentsPrompt) {
4532
+ function buildLightweightPagePrompt(pageName, route, styleContext, sharedComponentsSummary, pageType, tieredComponentsPrompt, reusePlanDirective) {
4524
4533
  const designConstraints = pageType ? getDesignQualityForType(pageType) : "";
4525
- const sharedNote = tieredComponentsPrompt || (sharedComponentsSummary ? `Available shared components:
4534
+ const sharedNote = reusePlanDirective || tieredComponentsPrompt || (sharedComponentsSummary ? `Available shared components:
4526
4535
  ${sharedComponentsSummary}` : "");
4527
4536
  return [
4528
4537
  `Generate complete pageCode for a page called "${pageName}" at route "${route}".`,
@@ -5050,10 +5059,10 @@ function extractComponentIdsFromCode(code) {
5050
5059
  return ids;
5051
5060
  }
5052
5061
  async function warnInlineDuplicates(projectRoot, pageName, route, pageCode, manifest, plan) {
5053
- const sectionOrWidget = manifest.shared.filter((e) => e.type === "section" || e.type === "widget");
5054
- if (sectionOrWidget.length === 0) return;
5062
+ const reusable = manifest.shared.filter((e) => e.type !== "layout");
5063
+ if (reusable.length === 0) return;
5055
5064
  const plannedForPage = plan ? new Set(plan.sharedComponents.filter((c) => c.usedBy.includes(route)).map((c) => c.name)) : null;
5056
- for (const e of sectionOrWidget) {
5065
+ for (const e of reusable) {
5057
5066
  if (plannedForPage && !plannedForPage.has(e.name)) continue;
5058
5067
  const kebab = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
5059
5068
  const hasImport = pageCode.includes(`@/components/shared/${kebab}`);
@@ -5298,6 +5307,52 @@ function extractPageNamesFromMessage(message) {
5298
5307
  }
5299
5308
  return pages;
5300
5309
  }
5310
+ function detectExplicitRootPage(message, pageNames) {
5311
+ const lower = message.toLowerCase();
5312
+ const w = "[a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u04010-9_]";
5313
+ const patterns = [
5314
+ new RegExp(
5315
+ `(?:main|start|home|root|first|entry|primary|\u0441\u0442\u0430\u0440\u0442\u043E\u0432${w}*|\u0433\u043B\u0430\u0432\u043D${w}*|\u043D\u0430\u0447\u0430\u043B\u044C\u043D${w}*)\\s*(?:page|screen|view|\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*|\u044D\u043A\u0440\u0430\u043D${w}*)\\s*(?:is|should be|:|\u2014|\u2013|-|\u0431\u0443\u0434\u0435\u0442|\u044D\u0442\u043E)\\s*([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+)`,
5316
+ "i"
5317
+ ),
5318
+ new RegExp(
5319
+ `([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+)\\s*(?:as|for|\u043A\u0430\u043A|\u0432 \u043A\u0430\u0447\u0435\u0441\u0442\u0432\u0435)\\s*(?:the\\s+)?(?:main|start|home|root|primary|\u0441\u0442\u0430\u0440\u0442\u043E\u0432${w}*|\u0433\u043B\u0430\u0432\u043D${w}*)\\s*(?:page|screen|\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*)`,
5320
+ "i"
5321
+ ),
5322
+ new RegExp(
5323
+ `(?:start|begin|\u043D\u0430\u0447\u0430\u0442${w}*|\u043D\u0430\u0447\u0438\u043D${w}*)\\s*(?:with|from|\u0441|\u0441\u043E)\\s*(?:a\\s+|an\\s+)?([a-z\u0430-\u044F\u0451A-Z\u0410-\u042F\u0401\\s]+?)(?:\\s+page|\\s+screen|\\s+form|\\s+\u0441\u0442\u0440\u0430\u043D\u0438\u0446${w}*|\\s+\u0444\u043E\u0440\u043C${w}*)?(?:\\s*$|[,.])`,
5324
+ "i"
5325
+ )
5326
+ ];
5327
+ for (const pattern of patterns) {
5328
+ const match = lower.match(pattern);
5329
+ if (match) {
5330
+ const keyword = match[1].trim();
5331
+ const found = pageNames.find(
5332
+ (p) => p.name.toLowerCase().includes(keyword) || keyword.includes(p.name.toLowerCase()) || p.id.includes(keyword.replace(/\s+/g, "-"))
5333
+ );
5334
+ if (found) return found.id;
5335
+ }
5336
+ }
5337
+ return null;
5338
+ }
5339
+ var NON_MARKETING_ROUTES = /* @__PURE__ */ new Set([
5340
+ "/login",
5341
+ "/signin",
5342
+ "/signup",
5343
+ "/register",
5344
+ "/forgot-password",
5345
+ "/reset-password",
5346
+ "/dashboard",
5347
+ "/settings",
5348
+ "/account",
5349
+ "/tasks",
5350
+ "/profile"
5351
+ ]);
5352
+ function isAppOnlyRequest(pageNames) {
5353
+ if (pageNames.length === 0) return false;
5354
+ return pageNames.every((p) => NON_MARKETING_ROUTES.has(p.route) || p.route.startsWith("/dashboard"));
5355
+ }
5301
5356
  function normalizeRequest(request, config2) {
5302
5357
  const changes = request.changes;
5303
5358
  const VALID_TYPES = [
@@ -5567,7 +5622,9 @@ import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync
5567
5622
  import { resolve as resolve6 } from "path";
5568
5623
  import { z } from "zod";
5569
5624
  import {
5625
+ SharedComponentTypeSchema,
5570
5626
  loadManifest as loadManifest5,
5627
+ saveManifest,
5571
5628
  generateSharedComponent as generateSharedComponent2
5572
5629
  } from "@getcoherent/core";
5573
5630
 
@@ -5688,6 +5745,140 @@ async function pMap(items, fn, concurrency = 3) {
5688
5745
 
5689
5746
  // src/commands/chat/split-generator.ts
5690
5747
  import chalk8 from "chalk";
5748
+
5749
+ // src/utils/reuse-planner.ts
5750
+ var SECTION_TYPE_MAP = {
5751
+ stats: ["data-display", "widget"],
5752
+ metrics: ["data-display", "widget"],
5753
+ kpi: ["data-display", "widget"],
5754
+ list: ["data-display"],
5755
+ table: ["data-display"],
5756
+ items: ["data-display"],
5757
+ form: ["form"],
5758
+ filter: ["form"],
5759
+ search: ["form"],
5760
+ nav: ["navigation"],
5761
+ menu: ["navigation"],
5762
+ tabs: ["navigation"],
5763
+ card: ["widget", "data-display"],
5764
+ grid: ["widget", "data-display"],
5765
+ chart: ["data-display"],
5766
+ graph: ["data-display"],
5767
+ alert: ["feedback"],
5768
+ toast: ["feedback"],
5769
+ banner: ["feedback"]
5770
+ };
5771
+ function sectionToComponentTypes(section) {
5772
+ const lower = section.toLowerCase();
5773
+ const types = /* @__PURE__ */ new Set();
5774
+ for (const [keyword, componentTypes] of Object.entries(SECTION_TYPE_MAP)) {
5775
+ if (lower.includes(keyword)) {
5776
+ componentTypes.forEach((t) => types.add(t));
5777
+ }
5778
+ }
5779
+ return [...types];
5780
+ }
5781
+ function componentFilenameToImportPath(file) {
5782
+ const withoutExt = file.replace(/\.tsx?$/, "");
5783
+ return `@/${withoutExt}`;
5784
+ }
5785
+ function buildReusePlan(input) {
5786
+ const { pageName, sections, manifest } = input;
5787
+ const reuse = [];
5788
+ const usedComponents = /* @__PURE__ */ new Set();
5789
+ for (const section of sections) {
5790
+ const matchingTypes = sectionToComponentTypes(section);
5791
+ if (matchingTypes.length === 0) continue;
5792
+ for (const entry of manifest.shared) {
5793
+ if (usedComponents.has(entry.id)) continue;
5794
+ if (!matchingTypes.includes(entry.type)) continue;
5795
+ reuse.push({
5796
+ component: entry.name,
5797
+ targetSection: section,
5798
+ reason: entry.usedIn.length > 0 ? `Used on ${entry.usedIn.length} page(s)` : `Matches section type (${entry.type})`,
5799
+ importPath: componentFilenameToImportPath(entry.file),
5800
+ usageExample: entry.usageExample || `<${entry.name} />`
5801
+ });
5802
+ usedComponents.add(entry.id);
5803
+ break;
5804
+ }
5805
+ }
5806
+ const reusePatterns = extractCodePatterns(input.existingPageCode, sections);
5807
+ return { pageName, reuse, createNew: [], reusePatterns };
5808
+ }
5809
+ function extractCodePatterns(existingPageCode, _sections) {
5810
+ const patterns = [];
5811
+ const gridPatterns = /* @__PURE__ */ new Map();
5812
+ for (const [route, code] of Object.entries(existingPageCode)) {
5813
+ const gridMatches = code.match(/className="[^"]*grid[^"]*"/g) || [];
5814
+ for (const match of gridMatches) {
5815
+ const cls = match.replace(/className="|"/g, "");
5816
+ if (!gridPatterns.has(cls)) gridPatterns.set(cls, []);
5817
+ gridPatterns.get(cls).push(route);
5818
+ }
5819
+ }
5820
+ for (const [pattern, pages] of gridPatterns) {
5821
+ if (pages.length >= 1 && pattern.includes("grid-cols")) {
5822
+ patterns.push({
5823
+ pattern,
5824
+ sourcePages: pages,
5825
+ targetSection: "Layout grid"
5826
+ });
5827
+ }
5828
+ }
5829
+ return patterns;
5830
+ }
5831
+ function buildReusePlanDirective(plan) {
5832
+ if (plan.reuse.length === 0 && plan.createNew.length === 0 && plan.reusePatterns.length === 0) {
5833
+ return "";
5834
+ }
5835
+ const lines = [`COMPONENT REUSE PLAN FOR THIS PAGE:`];
5836
+ if (plan.reuse.length > 0) {
5837
+ lines.push("", "MUST USE (import these \u2014 do NOT re-implement):");
5838
+ for (const r of plan.reuse) {
5839
+ lines.push(` - ${r.component} from ${r.importPath} \u2014 for "${r.targetSection}" section`);
5840
+ if (r.usageExample) {
5841
+ lines.push(` Example: ${r.usageExample}`);
5842
+ }
5843
+ }
5844
+ }
5845
+ if (plan.createNew.length > 0) {
5846
+ lines.push("", "CREATE NEW (no existing match):");
5847
+ for (const c of plan.createNew) {
5848
+ lines.push(` - ${c.name} \u2014 ${c.reason} (suggest type: ${c.suggestedType})`);
5849
+ }
5850
+ }
5851
+ if (plan.reusePatterns.length > 0) {
5852
+ lines.push("", "LAYOUT PATTERNS (copy from existing pages for visual consistency):");
5853
+ for (const p of plan.reusePatterns) {
5854
+ lines.push(` - ${p.targetSection}: className="${p.pattern}"`);
5855
+ lines.push(` (source: ${p.sourcePages.join(", ")})`);
5856
+ }
5857
+ }
5858
+ return lines.join("\n");
5859
+ }
5860
+ function verifyReusePlan(generatedCode, plan) {
5861
+ const passed = [];
5862
+ const missed = [];
5863
+ for (const entry of plan.reuse) {
5864
+ const isImported = generatedCode.includes(entry.importPath) || generatedCode.includes(`{ ${entry.component} }`) || generatedCode.includes(`{ ${entry.component},`);
5865
+ if (isImported) {
5866
+ passed.push(entry);
5867
+ } else {
5868
+ missed.push(entry);
5869
+ }
5870
+ }
5871
+ let retryDirective;
5872
+ if (missed.length > 0) {
5873
+ const lines = missed.map(
5874
+ (m) => `CRITICAL: Your previous output failed to import ${m.component} from ${m.importPath}. You MUST import and use this component for the "${m.targetSection}" section. Do NOT re-implement it inline.`
5875
+ );
5876
+ retryDirective = lines.join("\n");
5877
+ }
5878
+ return { passed, missed, retryDirective };
5879
+ }
5880
+
5881
+ // src/commands/chat/split-generator.ts
5691
5882
  function buildExistingPagesContext(config2) {
5692
5883
  const pages = config2.pages || [];
5693
5884
  const analyzed = pages.filter((p) => p.pageAnalysis);
@@ -5882,6 +6073,52 @@ function readExistingAppPageForReference(projectRoot, plan) {
5882
6073
  }
5883
6074
  return null;
5884
6075
  }
6076
+ function buildLayoutNote(layoutType) {
6077
+ switch (layoutType) {
6078
+ case "sidebar":
6079
+ return "This page uses a SIDEBAR layout. The sidebar navigation is already rendered by the group layout. Do NOT create your own sidebar or side navigation. Start with the main content area directly. The page content appears to the right of the sidebar.";
6080
+ case "both":
6081
+ return "This page has both a sidebar and a header rendered by the group layout. Do NOT include any site-wide header, nav, sidebar, or footer in this page. Start with the main content directly.";
6082
+ case "none":
6083
+ return "This page has no shared navigation from the layout. Include any needed navigation within the page itself.";
6084
+ default:
6085
+ return "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.";
6086
+ }
6087
+ }
6088
+ function buildAnchorPagePrompt(homePage, message, allPagesList, allRoutes, plan) {
6089
+ const pageType = detectPageType(homePage.name) || detectPageType(homePage.route);
6090
+ const authPageTypes = /* @__PURE__ */ new Set(["login", "register", "reset-password"]);
6091
+ const isAuth = isAuthRoute(homePage.route) || isAuthRoute(homePage.name) || authPageTypes.has(pageType || "");
6092
+ if (isAuth) {
6093
+ return `Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This is the application's entry point \u2014 a clean, centered authentication form. Generate complete pageCode. Do NOT include site-wide <header>, <nav>, or <footer> \u2014 this page has its own minimal layout. Make it visually polished with proper form validation UI \u2014 this page sets the design direction for the entire site. Do not generate other pages.`;
6094
+ }
6095
+ const groupLayout = plan?.groups.find((g) => g.pages.includes(homePage.route))?.layout;
6096
+ if (groupLayout === "sidebar" || pageType === "dashboard") {
6097
+ return `Create ONE page called "${homePage.name}" at route "${homePage.route}". Context: ${message}. This REPLACES the default placeholder page \u2014 generate a complete application page. Generate complete pageCode. Do NOT include a sidebar or top navigation \u2014 these are handled by the layout. Focus on the main content area. Make it visually polished \u2014 this page sets the design direction for the entire site. Do not generate other pages.`;
6098
+ }
6099
+ return `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.`;
6100
+ }
6101
+ function getGroupLayoutForRoute(route, plan) {
6102
+ if (!plan) return void 0;
6103
+ const group = plan.groups.find((g) => g.pages.includes(route));
6104
+ return group?.layout;
6105
+ }
6106
+ var manifestLock = Promise.resolve();
6107
+ async function updateManifestSafe(projectRoot, fn) {
6108
+ const timeoutMs = 5e3;
6109
+ const update = manifestLock.then(async () => {
6110
+ const m = await loadManifest5(projectRoot);
6111
+ const updated = fn(m);
6112
+ await saveManifest(projectRoot, updated);
6113
+ });
6114
+ manifestLock = update.catch(() => {
6115
+ });
6116
+ await Promise.race([
6117
+ update,
6118
+ new Promise((_, reject) => setTimeout(() => reject(new Error("manifest sync timeout")), timeoutMs))
6119
+ ]).catch(() => {
6120
+ });
6121
+ }
5885
6122
  async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
5886
6123
  let pageNames = [];
5887
6124
  spinner.start("Phase 1/6 \u2014 Planning pages...");
@@ -5924,7 +6161,11 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
5924
6161
  (p) => p.id !== "home" && p.id !== "new" && p.route !== "/"
5925
6162
  );
5926
6163
  const isFreshProject = userPages.length === 0;
5927
- if (isFreshProject || impliesFullWebsite(message)) {
6164
+ const explicitRootId = detectExplicitRootPage(message, pageNames);
6165
+ if (explicitRootId) {
6166
+ const rootPage = pageNames.find((p) => p.id === explicitRootId);
6167
+ if (rootPage) rootPage.route = "/";
6168
+ } else if (!isAppOnlyRequest(pageNames) && (isFreshProject || impliesFullWebsite(message))) {
5928
6169
  pageNames.unshift({ name: "Home", id: "home", route: "/" });
5929
6170
  }
5930
6171
  }
@@ -5943,14 +6184,16 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
5943
6184
  spinner.start("Phase 2/6 \u2014 Generating architecture plan...");
5944
6185
  try {
5945
6186
  const ai = await createAIProvider(provider ?? "auto");
5946
- const layoutHint = modCtx.config.navigation?.type || null;
5947
- const { plan: generatedPlan, warnings: planWarnings } = await generateArchitecturePlan(
5948
- pageNames,
5949
- message,
5950
- ai,
5951
- layoutHint
5952
- );
5953
- plan = generatedPlan;
6187
+ const cachedPlan = loadPlan(parseOpts.projectRoot);
6188
+ let planWarnings = [];
6189
+ if (cachedPlan) {
6190
+ plan = await updateArchitecturePlan(cachedPlan, pageNames, message, ai);
6191
+ } else {
6192
+ const layoutHint = modCtx.config.navigation?.type || null;
6193
+ const result = await generateArchitecturePlan(pageNames, message, ai, layoutHint);
6194
+ plan = result.plan;
6195
+ planWarnings = result.warnings;
6196
+ }
5954
6197
  if (plan) {
5955
6198
  const groupsSummary = plan.groups.map((g) => `${g.id} (${g.layout}, ${g.pages.length} pages)`).join(", ");
5956
6199
  const sharedSummary = plan.sharedComponents.length > 0 ? plan.sharedComponents.map((c) => `${c.name} \u2192 ${c.usedBy.join(", ")}`).join(" | ") : "";
@@ -6001,12 +6244,8 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
6001
6244
  if (!reusedExistingAnchor) {
6002
6245
  spinner.start(`Phase 3/6 \u2014 Generating ${homePage.name} page (sets design direction)...`);
6003
6246
  try {
6004
- const homeResult = await parseModification(
6005
- `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.`,
6006
- modCtx,
6007
- provider,
6008
- parseOpts
6009
- );
6247
+ const anchorPrompt = buildAnchorPagePrompt(homePage, message, allPagesList, allRoutes, plan);
6248
+ const homeResult = await parseModification(anchorPrompt, modCtx, provider, parseOpts);
6010
6249
  const codePage = homeResult.requests.find((r) => r.type === "add-page");
6011
6250
  if (codePage) {
6012
6251
  homeRequest = codePage;
@@ -6036,7 +6275,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
6036
6275
  if (plan && plan.sharedComponents.length > 0) {
6037
6276
  spinner.start(`Phase 4.5/6 \u2014 Generating ${plan.sharedComponents.length} shared components from plan...`);
6038
6277
  try {
6039
- const { generateSharedComponentsFromPlan } = await import("./plan-generator-H55WEIY2.js");
6278
+ const { generateSharedComponentsFromPlan } = await import("./plan-generator-ITHYNYJI.js");
6040
6279
  const generated = await generateSharedComponentsFromPlan(
6041
6280
  plan,
6042
6281
  styleContext,
@@ -6078,7 +6317,6 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
6078
6317
  return { requests: homeRequest ? [homeRequest] : [], plan };
6079
6318
  }
6080
6319
  spinner.start(`Phase 5/6 \u2014 Generating ${remainingPages.length} pages in parallel...`);
6081
- const sharedLayoutNote = "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.";
6082
6320
  const sharedComponentsNote = buildSharedComponentsNote(parseOpts.sharedComponentsSummary);
6083
6321
  const currentManifest = projectRoot ? await loadManifest5(projectRoot) : null;
6084
6322
  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="#".`;
@@ -6092,6 +6330,23 @@ ${existingAppPageCode}
6092
6330
  \`\`\`
6093
6331
  ` : "";
6094
6332
  const existingPagesContext = buildExistingPagesContext(modCtx.config);
6333
+ const existingPageCode = {};
6334
+ if (projectRoot) {
6335
+ const appDir = resolve6(projectRoot, "app");
6336
+ if (existsSync14(appDir)) {
6337
+ const pageFiles = readdirSync2(appDir, { recursive: true }).filter(
6338
+ (f) => typeof f === "string" && f.endsWith("page.tsx")
6339
+ );
6340
+ for (const pf of pageFiles) {
6341
+ try {
6342
+ const code = readFileSync9(resolve6(appDir, pf), "utf-8");
6343
+ const route = "/" + pf.replace(/\/page\.tsx$/, "").replace(/\(.*?\)\//g, "");
6344
+ existingPageCode[route === "/" ? "/" : route] = code;
6345
+ } catch {
6346
+ }
6347
+ }
6348
+ }
6349
+ }
6095
6350
  const AI_CONCURRENCY = 3;
6096
6351
  let phase5Done = 0;
6097
6352
  const remainingRequests = await pMap(
@@ -6101,15 +6356,45 @@ ${existingAppPageCode}
6101
6356
  const pageType = plan ? getPageType(route, plan) : inferPageTypeFromRoute(route);
6102
6357
  const designConstraints = getDesignQualityForType(pageType);
6103
6358
  const authNote = isAuth ? 'For this auth page: the auth layout already provides centering (flex items-center justify-center min-h-svh). Do NOT add your own centering wrapper or min-h-svh. Just output a div with className="w-full max-w-md" containing the Card. Do NOT use section containers or full-width wrappers.' : void 0;
6359
+ const layoutForPage = getGroupLayoutForRoute(route, plan);
6360
+ const layoutNote = buildLayoutNote(layoutForPage);
6104
6361
  const tieredNote = currentManifest ? buildTieredComponentsPrompt(currentManifest, pageType) : void 0;
6362
+ const pageKey = route.replace(/^\//, "") || "home";
6363
+ const pageSections = plan?.pageNotes?.[pageKey]?.sections || [];
6364
+ let reusePlanDirective = "";
6365
+ let currentReusePlan = null;
6366
+ if (currentManifest && currentManifest.shared.length > 0) {
6367
+ try {
6368
+ currentReusePlan = buildReusePlan({
6369
+ pageName: name,
6370
+ pageType,
6371
+ sections: pageSections,
6372
+ manifest: currentManifest,
6373
+ existingPageCode,
6374
+ userRequest: message
6375
+ });
6376
+ reusePlanDirective = buildReusePlanDirective(currentReusePlan);
6377
+ if (currentReusePlan.reuse.length > 0 || currentReusePlan.createNew.length > 0) {
6378
+ const parts = [];
6379
+ if (currentReusePlan.reuse.length > 0)
6380
+ parts.push(`REUSE: ${currentReusePlan.reuse.map((r) => r.component).join(", ")}`);
6381
+ if (currentReusePlan.createNew.length > 0)
6382
+ parts.push(`CREATE: ${currentReusePlan.createNew.map((c) => c.name).join(", ")}`);
6383
+ if (currentReusePlan.reusePatterns.length > 0)
6384
+ parts.push(`${currentReusePlan.reusePatterns.length} pattern(s)`);
6385
+ console.log(chalk8.dim(` \u{1F504} Reuse Plan for "${name}": ${parts.join(" | ")}`));
6386
+ }
6387
+ } catch {
6388
+ }
6389
+ }
6105
6390
  const prompt = [
6106
6391
  `Create ONE page called "${name}" at route "${route}".`,
6107
6392
  `Context: ${message}.`,
6108
6393
  `Generate complete pageCode for this single page only. Do not generate other pages.`,
6109
6394
  `PAGE TYPE: ${pageType}`,
6110
6395
  designConstraints,
6111
- sharedLayoutNote,
6112
- tieredNote || sharedComponentsNote,
6396
+ layoutNote,
6397
+ reusePlanDirective || tieredNote || sharedComponentsNote,
6113
6398
  routeNote,
6114
6399
  alignmentNote,
6115
6400
  authNote,
@@ -6122,7 +6407,47 @@ ${existingAppPageCode}
6122
6407
  const result = await parseModification(prompt, modCtx, provider, parseOpts);
6123
6408
  phase5Done++;
6124
6409
  spinner.text = `Phase 5/6 \u2014 ${phase5Done}/${remainingPages.length} pages generated...`;
6125
- const codePage = result.requests.find((r) => r.type === "add-page");
6410
+ let codePage = result.requests.find((r) => r.type === "add-page");
6411
+ if (currentReusePlan && currentReusePlan.reuse.length > 0 && codePage) {
6412
+ const pageCode = codePage.changes?.pageCode;
6413
+ if (pageCode) {
6414
+ const verification = verifyReusePlan(pageCode, currentReusePlan);
6415
+ if (verification.passed.length > 0) {
6416
+ console.log(
6417
+ chalk8.dim(` \u2713 Reuse verified for "${name}": ${verification.passed.map((p) => p.component).join(", ")}`)
6418
+ );
6419
+ }
6420
+ if (verification.missed.length > 0 && verification.retryDirective) {
6421
+ console.log(
6422
+ chalk8.yellow(
6423
+ ` \u26A0 Missed reuse in "${name}": ${verification.missed.map((m) => m.component).join(", ")} \u2014 retrying...`
6424
+ )
6425
+ );
6426
+ try {
6427
+ const retryPrompt = [prompt, verification.retryDirective].join("\n\n");
6428
+ const retryResult = await parseModification(retryPrompt, modCtx, provider, parseOpts);
6429
+ const retryPage = retryResult.requests.find((r) => r.type === "add-page");
6430
+ if (retryPage) codePage = retryPage;
6431
+ } catch {
6432
+ }
6433
+ }
6434
+ }
6435
+ }
6436
+ if (projectRoot && codePage && currentManifest) {
6437
+ const finalPageCode = codePage.changes?.pageCode;
6438
+ if (finalPageCode) {
6439
+ await updateManifestSafe(projectRoot, (m) => {
6440
+ const updatedShared = m.shared.map((entry) => {
6441
+ const isUsed = finalPageCode.includes(`{ ${entry.name} }`) || finalPageCode.includes(`{ ${entry.name},`);
6442
+ if (isUsed && !entry.usedIn.includes(route)) {
6443
+ return { ...entry, usedIn: [...entry.usedIn, route] };
6444
+ }
6445
+ return entry;
6446
+ });
6447
+ return { ...m, shared: updatedShared };
6448
+ });
6449
+ }
6450
+ }
6126
6451
  return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
6127
6452
  } catch {
6128
6453
  phase5Done++;
@@ -6170,7 +6495,7 @@ ${existingAppPageCode}
6170
6495
  }
6171
6496
  var SharedExtractionItemSchema = z.object({
6172
6497
  name: z.string().min(2).max(50),
6173
- type: z.enum(["section", "widget"]),
6498
+ type: SharedComponentTypeSchema,
6174
6499
  description: z.string().max(200).default(""),
6175
6500
  propsInterface: z.string().default("{}"),
6176
6501
  code: z.string()
@@ -6216,7 +6541,8 @@ async function extractSharedComponents(homePageCode, projectRoot, aiProvider) {
6216
6541
  const provider = getComponentProvider();
6217
6542
  for (const item of filtered) {
6218
6543
  try {
6219
- const { code: fixedCode } = await autoFixCode(item.code);
6544
+ let { code: fixedCode } = await autoFixCode(item.code);
6545
+ fixedCode = fixedCode.replace(/export default function (\w+)/g, "export function $1");
6220
6546
  const shadcnImports = [...fixedCode.matchAll(/from\s+["']@\/components\/ui\/(.+?)["']/g)];
6221
6547
  for (const match of shadcnImports) {
6222
6548
  await provider.installComponent(match[1], projectRoot);
@@ -6284,7 +6610,7 @@ import chalk11 from "chalk";
6284
6610
  import {
6285
6611
  getTemplateForPageType,
6286
6612
  loadManifest as loadManifest6,
6287
- saveManifest,
6613
+ saveManifest as saveManifest2,
6288
6614
  updateUsedIn,
6289
6615
  findSharedComponentByIdOrName,
6290
6616
  generateSharedComponent as generateSharedComponent4
@@ -6409,7 +6735,9 @@ async function canOverwriteShared(projectRoot, componentFile, storedHashes) {
6409
6735
  }
6410
6736
  return !edited;
6411
6737
  }
6412
- async function regenerateLayout(config2, projectRoot, options = { navChanged: false }) {
6738
+ async function regenerateLayout(config2, projectRoot, options = {
6739
+ navChanged: false
6740
+ }) {
6413
6741
  const appType = config2.settings.appType || "multi-page";
6414
6742
  const generator = new PageGenerator(config2);
6415
6743
  const initialized = config2.settings.initialized !== false;
@@ -6465,7 +6793,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
6465
6793
  try {
6466
6794
  await integrateSharedLayoutIntoRootLayout2(projectRoot);
6467
6795
  await ensureAuthRouteGroup(projectRoot);
6468
- await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type, options.navChanged);
6796
+ await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type, options.navChanged, options.groupLayouts);
6469
6797
  } catch (err) {
6470
6798
  if (process.env.COHERENT_DEBUG === "1") {
6471
6799
  console.log(chalk9.dim("Layout integration warning:", err));
@@ -6494,12 +6822,13 @@ async function scanAndInstallSharedDeps(projectRoot) {
6494
6822
  }
6495
6823
  return [...new Set(installed)];
6496
6824
  }
6497
- async function ensureAppRouteGroupLayout(projectRoot, navType, forceUpdate = false) {
6825
+ async function ensureAppRouteGroupLayout(projectRoot, navType, forceUpdate = false, groupLayouts) {
6826
+ const effectiveNavType = groupLayouts?.["app"] || navType;
6498
6827
  const layoutPath = resolve7(projectRoot, "app", "(app)", "layout.tsx");
6499
6828
  if (existsSync15(layoutPath) && !forceUpdate) return;
6500
6829
  const { mkdir: mkdirAsync } = await import("fs/promises");
6501
6830
  await mkdirAsync(resolve7(projectRoot, "app", "(app)"), { recursive: true });
6502
- const code = buildAppLayoutCode(navType);
6831
+ const code = buildAppLayoutCode(effectiveNavType);
6503
6832
  await writeFile(layoutPath, code);
6504
6833
  }
6505
6834
  function buildAppLayoutCode(navType) {
@@ -6583,15 +6912,32 @@ export default function GroupLayout({
6583
6912
  }
6584
6913
  `;
6585
6914
  }
6586
- async function ensurePlanGroupLayouts(projectRoot, plan) {
6915
+ async function ensurePlanGroupLayouts(projectRoot, plan, storedHashes = {}, config2) {
6587
6916
  const { mkdir: mkdirAsync } = await import("fs/promises");
6917
+ const { createHash: createHash2 } = await import("crypto");
6588
6918
  for (const group of plan.groups) {
6589
6919
  const groupDir = resolve7(projectRoot, "app", `(${group.id})`);
6590
6920
  await mkdirAsync(groupDir, { recursive: true });
6591
6921
  const layoutPath = resolve7(groupDir, "layout.tsx");
6922
+ const relPath = `app/(${group.id})/layout.tsx`;
6923
+ if (existsSync15(layoutPath)) {
6924
+ const currentContent = readFileSync10(layoutPath, "utf-8");
6925
+ const currentHash = createHash2("md5").update(currentContent).digest("hex");
6926
+ const storedHash = storedHashes[relPath];
6927
+ if (storedHash && storedHash !== currentHash) {
6928
+ continue;
6929
+ }
6930
+ }
6592
6931
  const code = buildGroupLayoutCode(group.layout, group.pages);
6593
6932
  await writeFile(layoutPath, code);
6594
6933
  }
6934
+ if (config2) {
6935
+ const layouts = {};
6936
+ for (const group of plan.groups) {
6937
+ layouts[group.id] = group.layout;
6938
+ }
6939
+ config2.groupLayouts = layouts;
6940
+ }
6595
6941
  }
6596
6942
  async function regenerateFiles(modified, config2, projectRoot, options = { navChanged: false }) {
6597
6943
  const componentIds = /* @__PURE__ */ new Set();
@@ -7093,7 +7439,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7093
7439
  const filePathRel = routePath ? `app/${routePath}/page.tsx` : "app/page.tsx";
7094
7440
  if (!usedIn.includes(filePathRel)) {
7095
7441
  const nextManifest = updateUsedIn(manifest, resolved.id, [...usedIn, filePathRel]);
7096
- await saveManifest(projectRoot, nextManifest);
7442
+ await saveManifest2(projectRoot, nextManifest);
7097
7443
  }
7098
7444
  printLinkSharedReport({
7099
7445
  sharedId: resolved.id,
@@ -7194,7 +7540,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7194
7540
  }
7195
7541
  const manifest = await loadManifest6(projectRoot);
7196
7542
  const nextManifest = updateUsedIn(manifest, created.id, usedInFiles);
7197
- await saveManifest(projectRoot, nextManifest);
7543
+ await saveManifest2(projectRoot, nextManifest);
7198
7544
  printPromoteAndLinkReport({
7199
7545
  id: created.id,
7200
7546
  name: created.name,
@@ -7413,44 +7759,55 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7413
7759
  });
7414
7760
  let issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
7415
7761
  const errors = issues.filter((i) => i.severity === "error");
7416
- if (errors.length >= 2 && aiProvider) {
7762
+ const MAX_QUALITY_FIX_ATTEMPTS = 2;
7763
+ let currentErrors = errors;
7764
+ for (let attempt = 0; attempt < MAX_QUALITY_FIX_ATTEMPTS && currentErrors.length > 0; attempt++) {
7765
+ if (!aiProvider) break;
7417
7766
  console.log(
7418
- chalk11.yellow(`
7419
- \u{1F504} ${errors.length} quality errors \u2014 attempting AI fix for ${page.name || page.id}...`)
7767
+ chalk11.yellow(
7768
+ `
7769
+ \u{1F504} ${currentErrors.length} quality errors \u2014 attempting AI fix${attempt > 0 ? ` (retry ${attempt + 1})` : ""} for ${page.name || page.id}...`
7770
+ )
7420
7771
  );
7421
7772
  try {
7422
7773
  const ai = await createAIProvider(aiProvider);
7423
- if (ai.editPageCode) {
7424
- const errorList = errors.map((e) => `Line ${e.line}: [${e.type}] ${e.message}`).join("\n");
7425
- const instruction = `Fix these quality issues:
7774
+ if (!ai.editPageCode) break;
7775
+ const errorList = currentErrors.map((e) => `Line ${e.line}: [${e.type}] ${e.message}`).join("\n");
7776
+ const instruction = `Fix these quality issues:
7426
7777
  ${errorList}
7427
7778
 
7428
7779
  Rules:
7429
7780
  - Replace raw Tailwind colors (bg-emerald-500, text-zinc-400, etc.) with semantic tokens (bg-primary, text-muted-foreground, bg-muted, etc.)
7781
+ - Replace placeholder content ("Lorem ipsum", "John Doe", "user@example.com") with realistic contextual content
7430
7782
  - Ensure heading hierarchy (h1 \u2192 h2 \u2192 h3, no skipping)
7431
7783
  - Add Label components for form inputs
7432
7784
  - Keep all existing functionality and layout intact`;
7433
- const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
7434
- if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
7435
- const recheck = validatePageQuality(fixedCode2, void 0, qualityPageType);
7436
- const recheckErrors = recheck.filter((i) => i.severity === "error");
7437
- if (recheckErrors.length < errors.length) {
7438
- codeToWrite = fixedCode2;
7439
- const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite, autoFixCtx);
7440
- if (reFixes.length > 0) {
7441
- codeToWrite = reFixed;
7442
- postFixes.push(...reFixes);
7443
- }
7444
- await writeFile(filePath, codeToWrite);
7445
- issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
7446
- const finalErrors = issues.filter((i) => i.severity === "error").length;
7447
- console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${finalErrors} errors`));
7785
+ const fixedCode2 = await ai.editPageCode(codeToWrite, instruction, page.name || page.id || "Page");
7786
+ if (fixedCode2 && fixedCode2.length > 100 && /export\s+default/.test(fixedCode2)) {
7787
+ const recheck = validatePageQuality(fixedCode2, void 0, qualityPageType);
7788
+ const recheckErrors = recheck.filter((i) => i.severity === "error");
7789
+ if (recheckErrors.length < currentErrors.length) {
7790
+ codeToWrite = fixedCode2;
7791
+ const { code: reFixed, fixes: reFixes } = await autoFixCode(codeToWrite, autoFixCtx);
7792
+ if (reFixes.length > 0) {
7793
+ codeToWrite = reFixed;
7794
+ postFixes.push(...reFixes);
7448
7795
  }
7796
+ await writeFile(filePath, codeToWrite);
7797
+ currentErrors = recheckErrors;
7798
+ console.log(chalk11.green(` \u2714 Quality fix: ${errors.length} \u2192 ${currentErrors.length} errors`));
7799
+ if (currentErrors.length === 0) break;
7800
+ } else {
7801
+ break;
7449
7802
  }
7803
+ } else {
7804
+ break;
7450
7805
  }
7451
7806
  } catch {
7807
+ break;
7452
7808
  }
7453
7809
  }
7810
+ issues = validatePageQuality(codeToWrite, void 0, qualityPageType);
7454
7811
  const report = formatIssues(issues);
7455
7812
  if (report) {
7456
7813
  console.log(chalk11.yellow(`
@@ -8056,7 +8413,7 @@ async function chatCommand(message, options) {
8056
8413
  if (options.newComponent) {
8057
8414
  const componentName = options.newComponent;
8058
8415
  spinner.start(`Creating shared component: ${componentName}...`);
8059
- const { createAIProvider: createAIProvider2 } = await import("./ai-provider-HUQO64P3.js");
8416
+ const { createAIProvider: createAIProvider2 } = await import("./ai-provider-CGSIYFZT.js");
8060
8417
  const { generateSharedComponent: generateSharedComponent7 } = await import("@getcoherent/core");
8061
8418
  const { autoFixCode: autoFixCode2 } = await import("./quality-validator-3K5BMJSR.js");
8062
8419
  const { extractPropsInterface, extractDependencies } = await import("./component-extractor-VYJLT5NR.js");
@@ -8102,7 +8459,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8102
8459
  type: classifications[0].type,
8103
8460
  description: classifications[0].description || message
8104
8461
  });
8105
- await saveManifest2(projectRoot, manifest2);
8462
+ await saveManifest3(projectRoot, manifest2);
8106
8463
  }
8107
8464
  } catch {
8108
8465
  }
@@ -8171,7 +8528,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8171
8528
  if (validShared.length !== manifest.shared.length) {
8172
8529
  const cleaned = manifest.shared.length - validShared.length;
8173
8530
  manifest = { ...manifest, shared: validShared };
8174
- await saveManifest2(project.root, manifest);
8531
+ await saveManifest3(project.root, manifest);
8175
8532
  if (DEBUG4) {
8176
8533
  console.log(chalk13.dim(`[pre-gen] Cleaned ${cleaned} orphaned component(s) from manifest`));
8177
8534
  }
@@ -8184,7 +8541,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8184
8541
  let uxRecommendations;
8185
8542
  const SPLIT_THRESHOLD = 4;
8186
8543
  const parseOpts = { sharedComponentsSummary, projectRoot };
8187
- const modCtx = { config: config2, componentManager: cm };
8544
+ const modCtx = { config: dsm.getConfig(), componentManager: cm };
8188
8545
  const multiPageHint = /\b(pages?|sections?)\s*[:]\s*\w/i.test(message) || (message.match(
8189
8546
  /\b(?:registration|about|catal|account|contact|pricing|dashboard|settings|login|sign.?up|blog|portfolio|features)\b/gi
8190
8547
  ) || []).length >= SPLIT_THRESHOLD;
@@ -8194,7 +8551,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8194
8551
  requests = splitResult.requests;
8195
8552
  if (splitResult.plan && projectRoot) {
8196
8553
  savePlan(projectRoot, splitResult.plan);
8197
- await ensurePlanGroupLayouts(projectRoot, splitResult.plan);
8554
+ await ensurePlanGroupLayouts(projectRoot, splitResult.plan, storedHashes, dsm.getConfig());
8198
8555
  }
8199
8556
  uxRecommendations = void 0;
8200
8557
  } catch {
@@ -8233,8 +8590,24 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8233
8590
  }
8234
8591
  }
8235
8592
  } else {
8593
+ let reusePlanDirective;
8236
8594
  try {
8237
- const result = await parseModification(message, modCtx, provider, parseOpts);
8595
+ const singlePageManifest = await loadManifest8(projectRoot);
8596
+ if (singlePageManifest.shared.length > 0) {
8597
+ const reusePlan = buildReusePlan({
8598
+ pageName: "page",
8599
+ pageType: inferPageTypeFromRoute("/"),
8600
+ sections: [],
8601
+ manifest: singlePageManifest,
8602
+ existingPageCode: {},
8603
+ userRequest: message
8604
+ });
8605
+ reusePlanDirective = buildReusePlanDirective(reusePlan) || void 0;
8606
+ }
8607
+ } catch {
8608
+ }
8609
+ try {
8610
+ const result = await parseModification(message, modCtx, provider, { ...parseOpts, reusePlanDirective });
8238
8611
  requests = result.requests;
8239
8612
  uxRecommendations = result.uxRecommendations;
8240
8613
  const pagesWithoutCode = requests.filter(
@@ -8272,7 +8645,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8272
8645
  requests = splitResult.requests;
8273
8646
  if (splitResult.plan && projectRoot) {
8274
8647
  savePlan(projectRoot, splitResult.plan);
8275
- await ensurePlanGroupLayouts(projectRoot, splitResult.plan);
8648
+ await ensurePlanGroupLayouts(projectRoot, splitResult.plan, storedHashes, dsm.getConfig());
8276
8649
  }
8277
8650
  uxRecommendations = void 0;
8278
8651
  } catch {
@@ -8798,7 +9171,7 @@ Return JSON: { "requests": [{ "type": "add-page", "changes": { "name": "${compon
8798
9171
  }
8799
9172
  }
8800
9173
  if (manifestChanged) {
8801
- await saveManifest2(projectRoot, currentManifest);
9174
+ await saveManifest3(projectRoot, currentManifest);
8802
9175
  if (DEBUG4) console.log(chalk13.dim("[auto-sync] Manifest updated"));
8803
9176
  }
8804
9177
  } catch {
@@ -8969,7 +9342,7 @@ import { DesignSystemManager as DesignSystemManager8, ComponentGenerator as Comp
8969
9342
  // src/utils/file-watcher.ts
8970
9343
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, existsSync as existsSync19 } from "fs";
8971
9344
  import { relative as relative4, join as join13 } from "path";
8972
- import { loadManifest as loadManifest9, saveManifest as saveManifest3 } from "@getcoherent/core";
9345
+ import { loadManifest as loadManifest9, saveManifest as saveManifest4 } from "@getcoherent/core";
8973
9346
 
8974
9347
  // src/utils/component-integrity.ts
8975
9348
  import { existsSync as existsSync18, readFileSync as readFileSync13, readdirSync as readdirSync5 } from "fs";
@@ -9357,7 +9730,7 @@ async function handleFileDelete(projectRoot, filePath) {
9357
9730
  ...manifest,
9358
9731
  shared: manifest.shared.filter((s) => s.id !== orphaned.id)
9359
9732
  };
9360
- await saveManifest3(projectRoot, cleaned);
9733
+ await saveManifest4(projectRoot, cleaned);
9361
9734
  console.log(chalk33.cyan(`
9362
9735
  \u{1F5D1} Auto-removed ${orphaned.id} (${orphaned.name}) \u2014 file deleted`));
9363
9736
  await writeCursorRules(projectRoot);
@@ -9594,6 +9967,41 @@ async function fixMissingComponentExports(projectRoot) {
9594
9967
  }
9595
9968
  }
9596
9969
  }
9970
+ const neededSharedExports = /* @__PURE__ */ new Map();
9971
+ for (const file of pages) {
9972
+ const content = readFileSync15(file, "utf-8");
9973
+ const sharedImportRe = /import\s*\{([^}]+)\}\s*from\s*['"]@\/components\/shared\/([^'"]+)['"]/g;
9974
+ let sm;
9975
+ while ((sm = sharedImportRe.exec(content)) !== null) {
9976
+ const names = sm[1].split(",").map((s) => s.trim()).filter(Boolean);
9977
+ const componentId = sm[2];
9978
+ if (!neededSharedExports.has(componentId)) neededSharedExports.set(componentId, /* @__PURE__ */ new Set());
9979
+ for (const name of names) neededSharedExports.get(componentId).add(name);
9980
+ }
9981
+ }
9982
+ for (const [componentId, needed] of neededSharedExports) {
9983
+ const componentFile = join14(sharedDir, `${componentId}.tsx`);
9984
+ if (!existsSync20(componentFile)) continue;
9985
+ let content = readFileSync15(componentFile, "utf-8");
9986
+ const exportRe = /export\s+(?:const|function|class)\s+(\w+)|export\s*\{([^}]+)\}/g;
9987
+ const existingExports = /* @__PURE__ */ new Set();
9988
+ let em;
9989
+ while ((em = exportRe.exec(content)) !== null) {
9990
+ if (em[1]) existingExports.add(em[1]);
9991
+ if (em[2])
9992
+ em[2].split(",").map(
9993
+ (s) => s.trim().split(/\s+as\s+/).pop()
9994
+ ).filter(Boolean).forEach((n) => existingExports.add(n));
9995
+ }
9996
+ const missing = [...needed].filter((n) => !existingExports.has(n));
9997
+ if (missing.length === 0) continue;
9998
+ const defaultExportMatch = content.match(/export\s+default\s+function\s+(\w+)/);
9999
+ if (defaultExportMatch && missing.includes(defaultExportMatch[1])) {
10000
+ content = content.replace(/export\s+default\s+function\s+(\w+)/, "export function $1");
10001
+ writeFileSync10(componentFile, content, "utf-8");
10002
+ console.log(chalk14.dim(` \u2714 Fixed export in ${componentId}.tsx (default \u2192 named)`));
10003
+ }
10004
+ }
9597
10005
  }
9598
10006
  async function backfillPageAnalysis(projectRoot) {
9599
10007
  const configPath = join14(projectRoot, "design-system.config.ts");
@@ -10318,7 +10726,7 @@ import {
10318
10726
  PageManager as PageManager4,
10319
10727
  ComponentGenerator as ComponentGenerator4,
10320
10728
  loadManifest as loadManifest10,
10321
- saveManifest as saveManifest4
10729
+ saveManifest as saveManifest5
10322
10730
  } from "@getcoherent/core";
10323
10731
  function extractComponentIdsFromCode2(code) {
10324
10732
  const ids = /* @__PURE__ */ new Set();
@@ -10587,7 +10995,7 @@ async function fixCommand(opts = {}) {
10587
10995
  manifestModified = true;
10588
10996
  }
10589
10997
  if (manifestModified && !dryRun) {
10590
- await saveManifest4(project.root, manifest);
10998
+ await saveManifest5(project.root, manifest);
10591
10999
  fixes.push("Shared component manifest updated");
10592
11000
  }
10593
11001
  for (const entry of manifest.shared) {
@@ -11124,10 +11532,15 @@ function createComponentsCommand() {
11124
11532
  });
11125
11533
  console.log(chalk25.cyan("\u{1F4A1} Modify by ID:"), chalk25.white('coherent chat "in CID-001 add a search button"\n'));
11126
11534
  });
11127
- sharedCmd.command("add <name>").description("Create a shared component (layout/section/widget) and register in manifest").option("-t, --type <type>", "Type: layout | section | widget", "layout").option("-d, --description <desc>", "Description").action(async (name, opts) => {
11535
+ sharedCmd.command("add <name>").description("Create a shared component (layout/section/widget) and register in manifest").option(
11536
+ "-t, --type <type>",
11537
+ "Type: layout | navigation | data-display | form | feedback | section | widget",
11538
+ "layout"
11539
+ ).option("-d, --description <desc>", "Description").action(async (name, opts) => {
11128
11540
  const project = findConfig();
11129
11541
  if (!project) exitNotCoherent();
11130
- const type = opts.type === "section" || opts.type === "widget" ? opts.type : "layout";
11542
+ const validTypes = ["layout", "navigation", "data-display", "form", "feedback", "section", "widget"];
11543
+ const type = validTypes.includes(opts.type ?? "") ? opts.type : "layout";
11131
11544
  const result = await generateSharedComponent5(project.root, {
11132
11545
  name: name.trim(),
11133
11546
  type,
@@ -11803,7 +12216,7 @@ import { existsSync as existsSync27, readFileSync as readFileSync19 } from "fs";
11803
12216
  import { join as join20, relative as relative5, dirname as dirname10 } from "path";
11804
12217
  import { readdir as readdir4, readFile as readFile7 } from "fs/promises";
11805
12218
  import { DesignSystemManager as DesignSystemManager16 } from "@getcoherent/core";
11806
- import { loadManifest as loadManifest13, saveManifest as saveManifest5, findSharedComponent } from "@getcoherent/core";
12219
+ import { loadManifest as loadManifest13, saveManifest as saveManifest6, findSharedComponent } from "@getcoherent/core";
11807
12220
  function extractTokensFromProject(projectRoot) {
11808
12221
  const lightColors = {};
11809
12222
  const darkColors = {};
@@ -12099,7 +12512,7 @@ async function syncCommand(options = {}) {
12099
12512
  const { manifest: reconciledManifest, result: rr } = reconcileComponents(project.root, manifest);
12100
12513
  reconcileResult = rr;
12101
12514
  if (!dryRun) {
12102
- await saveManifest5(project.root, reconciledManifest);
12515
+ await saveManifest6(project.root, reconciledManifest);
12103
12516
  }
12104
12517
  detectedComponents = await detectCustomComponents(project.root, allPageCode);
12105
12518
  for (const comp of detectedComponents) {
@@ -365,7 +365,7 @@ Rules:
365
365
  - Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
366
366
  - Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
367
367
 
368
- Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
368
+ Each component object: "name" (PascalCase), "type" ("layout"|"navigation"|"data-display"|"form"|"feedback"|"section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
369
369
 
370
370
  If no repeating patterns found: { "components": [] }`
371
371
  }
@@ -11,7 +11,7 @@ import {
11
11
  routeToKey,
12
12
  savePlan,
13
13
  updateArchitecturePlan
14
- } from "./chunk-WRDWFCQJ.js";
14
+ } from "./chunk-PVJJ2YXP.js";
15
15
  import "./chunk-5AHG4NNX.js";
16
16
  import "./chunk-3RG5ZIWI.js";
17
17
  export {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.14",
6
+ "version": "0.6.15",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -33,8 +33,15 @@
33
33
  ],
34
34
  "author": "Coherent Design Method",
35
35
  "license": "MIT",
36
+ "scripts": {
37
+ "dev": "tsup --watch",
38
+ "build": "tsup",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest"
41
+ },
36
42
  "dependencies": {
37
43
  "@anthropic-ai/sdk": "^0.32.0",
44
+ "@getcoherent/core": "workspace:*",
38
45
  "chalk": "^5.3.0",
39
46
  "chokidar": "^4.0.1",
40
47
  "commander": "^11.1.0",
@@ -42,19 +49,12 @@
42
49
  "open": "^10.1.0",
43
50
  "ora": "^7.0.1",
44
51
  "prompts": "^2.4.2",
45
- "zod": "^3.22.4",
46
- "@getcoherent/core": "0.6.14"
52
+ "zod": "^3.22.4"
47
53
  },
48
54
  "devDependencies": {
49
55
  "@types/node": "^20.11.0",
50
56
  "@types/prompts": "^2.4.9",
51
57
  "tsup": "^8.0.1",
52
58
  "typescript": "^5.3.3"
53
- },
54
- "scripts": {
55
- "dev": "tsup --watch",
56
- "build": "tsup",
57
- "typecheck": "tsc --noEmit",
58
- "test": "vitest"
59
59
  }
60
- }
60
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Sergei Kovtun
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.