@getcoherent/cli 0.5.13 → 0.5.14

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/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
@@ -357,6 +357,49 @@ Requirements:
357
357
  if (content.type !== "text") throw new Error("Unexpected response type");
358
358
  return content.text.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
359
359
  }
360
+ async extractSharedComponents(pageCode, reservedNames, existingSharedNames) {
361
+ try {
362
+ const response = await this.client.messages.create({
363
+ model: this.defaultModel,
364
+ max_tokens: 16384,
365
+ messages: [
366
+ {
367
+ role: "user",
368
+ content: `Analyze this page and extract reusable components.
369
+
370
+ PAGE CODE:
371
+ ${pageCode}
372
+
373
+ Rules:
374
+ - Extract 1-5 components maximum
375
+ - Each component must be \u226510 lines of meaningful JSX
376
+ - Output complete, self-contained TypeScript modules with:
377
+ - "use client" directive (if hooks or event handlers are used)
378
+ - All necessary imports (shadcn/ui from @/components/ui/*, lucide-react, next/link, etc.)
379
+ - A typed props interface exported as a named type
380
+ - A named export function (not default export)
381
+ - Do NOT extract: the entire page, trivial wrappers, layout components (header, footer, nav)
382
+ - Do NOT use these names (reserved for shadcn/ui): ${reservedNames.join(", ")}
383
+ - Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
384
+ - Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
385
+
386
+ Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
387
+
388
+ If no repeating patterns found: { "components": [] }`
389
+ }
390
+ ],
391
+ system: "You are a React/Next.js component extraction specialist. Analyze page code and identify reusable UI patterns that can be extracted into shared components. Return ONLY valid JSON. No markdown fencing, no explanation outside the JSON object."
392
+ });
393
+ const content = response.content[0];
394
+ if (content.type !== "text") return { components: [] };
395
+ const jsonText = this.extractJSON(content.text);
396
+ const parsed = JSON.parse(jsonText);
397
+ const components = Array.isArray(parsed.components) ? parsed.components : [];
398
+ return { components };
399
+ } catch {
400
+ return { components: [] };
401
+ }
402
+ }
360
403
  };
361
404
  export {
362
405
  ClaudeClient
package/dist/index.js CHANGED
@@ -3750,11 +3750,11 @@ import { resolve as resolve9, relative as relative2, join as join11 } from "path
3750
3750
  import { existsSync as existsSync16, readFileSync as readFileSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync2 } from "fs";
3751
3751
  import {
3752
3752
  DesignSystemManager as DesignSystemManager7,
3753
- ComponentManager as ComponentManager4,
3753
+ ComponentManager as ComponentManager5,
3754
3754
  PageManager as PageManager3,
3755
3755
  CLI_VERSION as CLI_VERSION2,
3756
3756
  getTemplateForPageType as getTemplateForPageType2,
3757
- loadManifest as loadManifest7,
3757
+ loadManifest as loadManifest8,
3758
3758
  saveManifest as saveManifest2
3759
3759
  } from "@getcoherent/core";
3760
3760
 
@@ -3831,7 +3831,7 @@ Please set ${envVar} in your environment or .env file.`);
3831
3831
  }
3832
3832
  if (preferredProvider === "openai") {
3833
3833
  try {
3834
- const { OpenAIClient } = await import("./openai-provider-3XC6CVZR.js");
3834
+ const { OpenAIClient } = await import("./openai-provider-FSXSVEYD.js");
3835
3835
  return await OpenAIClient.create(apiKey2, config2?.model);
3836
3836
  } catch (error) {
3837
3837
  if (error.message?.includes("not installed")) {
@@ -3845,7 +3845,7 @@ Error: ${error.message}`
3845
3845
  );
3846
3846
  }
3847
3847
  } else {
3848
- const { ClaudeClient } = await import("./claude-QH2XB2E3.js");
3848
+ const { ClaudeClient } = await import("./claude-RFHVT7RC.js");
3849
3849
  return ClaudeClient.create(apiKey2, config2?.model);
3850
3850
  }
3851
3851
  }
@@ -3858,7 +3858,7 @@ Error: ${error.message}`
3858
3858
  switch (provider) {
3859
3859
  case "openai":
3860
3860
  try {
3861
- const { OpenAIClient } = await import("./openai-provider-3XC6CVZR.js");
3861
+ const { OpenAIClient } = await import("./openai-provider-FSXSVEYD.js");
3862
3862
  return await OpenAIClient.create(apiKey, config2?.model);
3863
3863
  } catch (error) {
3864
3864
  if (error.message?.includes("not installed")) {
@@ -3872,7 +3872,7 @@ Error: ${error.message}`
3872
3872
  );
3873
3873
  }
3874
3874
  case "claude":
3875
- const { ClaudeClient } = await import("./claude-QH2XB2E3.js");
3875
+ const { ClaudeClient } = await import("./claude-RFHVT7RC.js");
3876
3876
  return ClaudeClient.create(apiKey, config2?.model);
3877
3877
  default:
3878
3878
  throw new Error(`Unsupported AI provider: ${provider}`);
@@ -7395,6 +7395,10 @@ function applyDefaults(request) {
7395
7395
  return request;
7396
7396
  }
7397
7397
 
7398
+ // src/commands/chat/split-generator.ts
7399
+ import { z } from "zod";
7400
+ import { loadManifest as loadManifest5, generateSharedComponent as generateSharedComponent2 } from "@getcoherent/core";
7401
+
7398
7402
  // src/utils/page-analyzer.ts
7399
7403
  var FORM_COMPONENTS = /* @__PURE__ */ new Set(["Input", "Textarea", "Label", "Select", "Checkbox", "Switch"]);
7400
7404
  var VISUAL_WORDS = /\b(grid lines?|glow|radial|gradient|blur|shadow|overlay|animation|particles?|dots?|vertical|horizontal|decorat|behind|background|divider|spacer|wrapper|container|inner|outer|absolute|relative|translate|opacity|z-index|transition)\b/i;
@@ -7589,9 +7593,20 @@ function parseNavTypeFromPlan(planResult) {
7589
7593
  }
7590
7594
  return "header";
7591
7595
  }
7596
+ function buildSharedComponentsSummary(manifest) {
7597
+ if (manifest.shared.length === 0) return void 0;
7598
+ return manifest.shared.map((e) => {
7599
+ const importPath = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
7600
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
7601
+ const propsLine = e.propsInterface ? `
7602
+ Props: ${e.propsInterface}` : "";
7603
+ return ` ${e.id} ${e.name} (${e.type})${desc}
7604
+ Import: @/components/shared/${importPath}${propsLine}`;
7605
+ }).join("\n");
7606
+ }
7592
7607
  async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts) {
7593
7608
  let pageNames = [];
7594
- spinner.start("Phase 1/4 \u2014 Planning pages...");
7609
+ spinner.start("Phase 1/5 \u2014 Planning pages...");
7595
7610
  try {
7596
7611
  const planResult = await parseModification(message, modCtx, provider, { ...parseOpts, planOnly: true });
7597
7612
  const pageReqs = planResult.requests.filter((r) => r.type === "add-page");
@@ -7644,7 +7659,7 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7644
7659
  const allRoutes = pageNames.map((p) => p.route).join(", ");
7645
7660
  const allPagesList = pageNames.map((p) => `${p.name} (${p.route})`).join(", ");
7646
7661
  const inferredNote = inferred.length > 0 ? ` (${inferred.length} auto-inferred)` : "";
7647
- spinner.succeed(`Phase 1/4 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
7662
+ spinner.succeed(`Phase 1/5 \u2014 Found ${pageNames.length} pages${inferredNote}: ${allPagesList}`);
7648
7663
  const homeIdx = pageNames.findIndex((p) => p.route === "/");
7649
7664
  const homePage = homeIdx !== -1 ? pageNames[homeIdx] : pageNames[0];
7650
7665
  const remainingPages = pageNames.filter((_, i) => i !== (homeIdx !== -1 ? homeIdx : 0));
@@ -7657,12 +7672,12 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7657
7672
  if (existingCode) {
7658
7673
  reusedExistingAnchor = true;
7659
7674
  homePageCode = existingCode;
7660
- spinner.start(`Phase 2/4 \u2014 Loading ${homePage.name} from disk (style anchor)...`);
7661
- spinner.succeed(`Phase 2/4 \u2014 Reused existing ${homePage.name} page (skipped AI regeneration)`);
7675
+ spinner.start(`Phase 2/5 \u2014 Loading ${homePage.name} from disk (style anchor)...`);
7676
+ spinner.succeed(`Phase 2/5 \u2014 Reused existing ${homePage.name} page (skipped AI regeneration)`);
7662
7677
  }
7663
7678
  }
7664
7679
  if (!reusedExistingAnchor) {
7665
- spinner.start(`Phase 2/4 \u2014 Generating ${homePage.name} page (sets design direction)...`);
7680
+ spinner.start(`Phase 2/5 \u2014 Generating ${homePage.name} page (sets design direction)...`);
7666
7681
  try {
7667
7682
  const homeResult = await parseModification(
7668
7683
  `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.`,
@@ -7684,21 +7699,40 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7684
7699
  changes: { id: homePage.id, name: homePage.name, route: homePage.route }
7685
7700
  };
7686
7701
  }
7687
- spinner.succeed(`Phase 2/4 \u2014 ${homePage.name} page generated`);
7702
+ spinner.succeed(`Phase 2/5 \u2014 ${homePage.name} page generated`);
7688
7703
  }
7689
- spinner.start("Phase 3/4 \u2014 Extracting design patterns...");
7704
+ spinner.start("Phase 3/5 \u2014 Extracting design patterns...");
7690
7705
  const styleContext = homePageCode ? extractStyleContext(homePageCode) : "";
7691
7706
  if (styleContext) {
7692
7707
  const lineCount = styleContext.split("\n").length - 1;
7693
7708
  const source = reusedExistingAnchor ? `${homePage.name} (existing file)` : homePage.name;
7694
- spinner.succeed(`Phase 3/4 \u2014 Extracted ${lineCount} style patterns from ${source}`);
7709
+ spinner.succeed(`Phase 3/5 \u2014 Extracted ${lineCount} style patterns from ${source}`);
7695
7710
  } else {
7696
- spinner.succeed("Phase 3/4 \u2014 No style patterns extracted (anchor page had no code)");
7711
+ spinner.succeed("Phase 3/5 \u2014 No style patterns extracted (anchor page had no code)");
7712
+ }
7713
+ if (remainingPages.length >= 2 && homePageCode && projectRoot) {
7714
+ const manifest = await loadManifest5(projectRoot);
7715
+ const shouldSkip = reusedExistingAnchor && manifest.shared.some((e) => e.type !== "layout");
7716
+ if (!shouldSkip) {
7717
+ spinner.start("Phase 3.5/5 \u2014 Extracting shared components...");
7718
+ try {
7719
+ const extraction = await extractSharedComponents(homePageCode, projectRoot, provider ?? "auto");
7720
+ parseOpts.sharedComponentsSummary = extraction.summary;
7721
+ if (extraction.components.length > 0) {
7722
+ const names = extraction.components.map((c) => c.name).join(", ");
7723
+ spinner.succeed(`Phase 3.5/5 \u2014 Extracted ${extraction.components.length} shared components (${names})`);
7724
+ } else {
7725
+ spinner.succeed("Phase 3.5/5 \u2014 No shared components extracted");
7726
+ }
7727
+ } catch {
7728
+ spinner.warn("Phase 3.5/5 \u2014 Could not extract shared components (continuing without)");
7729
+ }
7730
+ }
7697
7731
  }
7698
7732
  if (remainingPages.length === 0) {
7699
7733
  return homeRequest ? [homeRequest] : [];
7700
7734
  }
7701
- spinner.start(`Phase 4/4 \u2014 Generating ${remainingPages.length} pages in parallel...`);
7735
+ spinner.start(`Phase 4/5 \u2014 Generating ${remainingPages.length} pages in parallel...`);
7702
7736
  const sharedNote = "Header and Footer are shared components rendered by the root layout. Do NOT include any site-wide <header>, <nav>, or <footer> in this page. Start with the main content directly.";
7703
7737
  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="#".`;
7704
7738
  const alignmentNote = 'CRITICAL LAYOUT RULE: Every <section> must wrap its content in a container div matching the header width. Use the EXACT same container classes as shown in the style context (e.g. className="container max-w-6xl px-4" or className="max-w-6xl mx-auto px-4"). Inner content can use narrower max-w for text centering, but the outer section container MUST match.';
@@ -7721,12 +7755,12 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7721
7755
  try {
7722
7756
  const result = await parseModification(prompt, modCtx, provider, parseOpts);
7723
7757
  phase4Done++;
7724
- spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
7758
+ spinner.text = `Phase 4/5 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
7725
7759
  const codePage = result.requests.find((r) => r.type === "add-page");
7726
7760
  return codePage || { type: "add-page", target: "new", changes: { id, name, route } };
7727
7761
  } catch {
7728
7762
  phase4Done++;
7729
- spinner.text = `Phase 4/4 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
7763
+ spinner.text = `Phase 4/5 \u2014 ${phase4Done}/${remainingPages.length} pages generated...`;
7730
7764
  return { type: "add-page", target: "new", changes: { id, name, route } };
7731
7765
  }
7732
7766
  },
@@ -7757,9 +7791,75 @@ async function splitGeneratePages(spinner, message, modCtx, provider, parseOpts)
7757
7791
  }
7758
7792
  }
7759
7793
  const withCode = allRequests.filter((r) => r.changes?.pageCode).length;
7760
- spinner.succeed(`Phase 4/4 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
7794
+ spinner.succeed(`Phase 4/5 \u2014 Generated ${allRequests.length} pages (${withCode} with full code)`);
7761
7795
  return allRequests;
7762
7796
  }
7797
+ var SharedExtractionItemSchema = z.object({
7798
+ name: z.string().min(2).max(50),
7799
+ type: z.enum(["section", "widget"]),
7800
+ description: z.string().max(200).default(""),
7801
+ propsInterface: z.string().default("{}"),
7802
+ code: z.string()
7803
+ });
7804
+ var SharedExtractionResponseSchema = z.object({
7805
+ components: z.array(SharedExtractionItemSchema).max(5).default([])
7806
+ });
7807
+ async function extractSharedComponents(homePageCode, projectRoot, aiProvider) {
7808
+ const manifest = await loadManifest5(projectRoot);
7809
+ let ai;
7810
+ try {
7811
+ ai = await createAIProvider(aiProvider);
7812
+ } catch {
7813
+ return { components: [], summary: buildSharedComponentsSummary(manifest) };
7814
+ }
7815
+ if (!ai.extractSharedComponents) {
7816
+ return { components: [], summary: buildSharedComponentsSummary(manifest) };
7817
+ }
7818
+ let rawItems;
7819
+ try {
7820
+ const reservedNames = getComponentProvider().listNames();
7821
+ const existingNames = manifest.shared.map((e) => e.name);
7822
+ const result = await ai.extractSharedComponents(homePageCode, reservedNames, existingNames);
7823
+ const parsed = SharedExtractionResponseSchema.safeParse(result);
7824
+ rawItems = parsed.success ? parsed.data.components : [];
7825
+ } catch {
7826
+ return { components: [], summary: buildSharedComponentsSummary(manifest) };
7827
+ }
7828
+ const reservedSet = new Set(getComponentProvider().listNames().map((n) => n.toLowerCase()));
7829
+ const existingSet = new Set(manifest.shared.map((e) => e.name.toLowerCase()));
7830
+ const seenNames = /* @__PURE__ */ new Set();
7831
+ const filtered = rawItems.filter((item) => {
7832
+ if (item.code.split("\n").length < 10) return false;
7833
+ if (reservedSet.has(item.name.toLowerCase())) return false;
7834
+ if (existingSet.has(item.name.toLowerCase())) return false;
7835
+ if (seenNames.has(item.name.toLowerCase())) return false;
7836
+ seenNames.add(item.name.toLowerCase());
7837
+ return true;
7838
+ });
7839
+ const results = [];
7840
+ const provider = getComponentProvider();
7841
+ for (const item of filtered) {
7842
+ try {
7843
+ const { code: fixedCode } = await autoFixCode(item.code);
7844
+ const shadcnImports = [...fixedCode.matchAll(/from\s+["']@\/components\/ui\/(.+?)["']/g)];
7845
+ for (const match of shadcnImports) {
7846
+ await provider.installComponent(match[1], projectRoot);
7847
+ }
7848
+ const result = await generateSharedComponent2(projectRoot, {
7849
+ name: item.name,
7850
+ type: item.type,
7851
+ code: fixedCode,
7852
+ description: item.description,
7853
+ propsInterface: item.propsInterface,
7854
+ usedIn: []
7855
+ });
7856
+ results.push(result);
7857
+ } catch {
7858
+ }
7859
+ }
7860
+ const updatedManifest = await loadManifest5(projectRoot);
7861
+ return { components: results, summary: buildSharedComponentsSummary(updatedManifest) };
7862
+ }
7763
7863
  function extractAppNameFromPrompt(prompt) {
7764
7864
  const patterns = [
7765
7865
  /(?:called|named|app\s+name)\s+["']([^"']+)["']/i,
@@ -7807,11 +7907,11 @@ import { dirname as dirname6 } from "path";
7807
7907
  import chalk11 from "chalk";
7808
7908
  import {
7809
7909
  getTemplateForPageType,
7810
- loadManifest as loadManifest5,
7910
+ loadManifest as loadManifest6,
7811
7911
  saveManifest,
7812
7912
  updateUsedIn,
7813
7913
  findSharedComponentByIdOrName,
7814
- generateSharedComponent as generateSharedComponent3
7914
+ generateSharedComponent as generateSharedComponent4
7815
7915
  } from "@getcoherent/core";
7816
7916
 
7817
7917
  // src/commands/chat/code-generator.ts
@@ -7824,7 +7924,7 @@ import {
7824
7924
  PageGenerator,
7825
7925
  TailwindConfigGenerator
7826
7926
  } from "@getcoherent/core";
7827
- import { integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout2, generateSharedComponent as generateSharedComponent2 } from "@getcoherent/core";
7927
+ import { integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout2, generateSharedComponent as generateSharedComponent3 } from "@getcoherent/core";
7828
7928
  import chalk9 from "chalk";
7829
7929
 
7830
7930
  // src/utils/file-hashes.ts
@@ -7950,7 +8050,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
7950
8050
  if (navType === "header" || navType === "both") {
7951
8051
  if (await canOverwriteShared(projectRoot, "components/shared/header.tsx", hashes)) {
7952
8052
  const headerCode = generator.generateSharedHeaderCode();
7953
- await generateSharedComponent2(projectRoot, {
8053
+ await generateSharedComponent3(projectRoot, {
7954
8054
  name: "Header",
7955
8055
  type: "layout",
7956
8056
  code: headerCode,
@@ -7962,7 +8062,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
7962
8062
  }
7963
8063
  if (await canOverwriteShared(projectRoot, "components/shared/footer.tsx", hashes)) {
7964
8064
  const footerCode = generator.generateSharedFooterCode();
7965
- await generateSharedComponent2(projectRoot, {
8065
+ await generateSharedComponent3(projectRoot, {
7966
8066
  name: "Footer",
7967
8067
  type: "layout",
7968
8068
  code: footerCode,
@@ -7974,7 +8074,7 @@ async function regenerateLayout(config2, projectRoot, options = { navChanged: fa
7974
8074
  if (navType === "sidebar" || navType === "both") {
7975
8075
  if (await canOverwriteShared(projectRoot, "components/shared/sidebar.tsx", hashes)) {
7976
8076
  const sidebarCode = generator.generateSharedSidebarCode();
7977
- await generateSharedComponent2(projectRoot, {
8077
+ await generateSharedComponent3(projectRoot, {
7978
8078
  name: "AppSidebar",
7979
8079
  type: "layout",
7980
8080
  code: sidebarCode,
@@ -8526,7 +8626,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
8526
8626
  fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
8527
8627
  }
8528
8628
  await writeFile(pageFilePath, fixedCode);
8529
- const manifest = await loadManifest5(projectRoot);
8629
+ const manifest = await loadManifest6(projectRoot);
8530
8630
  const usedIn = manifest.shared.find((e) => e.id === resolved.id)?.usedIn ?? [];
8531
8631
  const routePath = route.replace(/^\//, "");
8532
8632
  const filePathRel = routePath ? `app/${routePath}/page.tsx` : "app/page.tsx";
@@ -8596,7 +8696,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
8596
8696
  };
8597
8697
  }
8598
8698
  const extractedCode = await ai.extractBlockAsComponent(sourceCode, blockHint, componentName);
8599
- const created = await generateSharedComponent3(projectRoot, {
8699
+ const created = await generateSharedComponent4(projectRoot, {
8600
8700
  name: componentName,
8601
8701
  type: "section",
8602
8702
  code: extractedCode,
@@ -8631,7 +8731,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
8631
8731
  await writeFile(fullPath, fixedCode);
8632
8732
  usedInFiles.push(relPath);
8633
8733
  }
8634
- const manifest = await loadManifest5(projectRoot);
8734
+ const manifest = await loadManifest6(projectRoot);
8635
8735
  const nextManifest = updateUsedIn(manifest, created.id, usedInFiles);
8636
8736
  await saveManifest(projectRoot, nextManifest);
8637
8737
  printPromoteAndLinkReport({
@@ -8820,7 +8920,7 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
8820
8920
  cm.updateConfig(cfg);
8821
8921
  pm.updateConfig(cfg);
8822
8922
  }
8823
- const manifestForAudit = await loadManifest5(projectRoot);
8923
+ const manifestForAudit = await loadManifest6(projectRoot);
8824
8924
  await warnInlineDuplicates(projectRoot, page.name || page.id || route.slice(1), codeToWrite, manifestForAudit);
8825
8925
  const relFilePath = routeToRelPath(route, isAuth);
8826
8926
  printPostGenerationReport({
@@ -9020,7 +9120,7 @@ ${pagesCtx}`
9020
9120
  cm.updateConfig(cfg);
9021
9121
  pm.updateConfig(cfg);
9022
9122
  }
9023
- const manifestForAudit = await loadManifest5(projectRoot);
9123
+ const manifestForAudit = await loadManifest6(projectRoot);
9024
9124
  await warnInlineDuplicates(
9025
9125
  projectRoot,
9026
9126
  pageDef.name || pageDef.id || route.slice(1),
@@ -9063,7 +9163,7 @@ ${pagesCtx}`
9063
9163
  fixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
9064
9164
  }
9065
9165
  const relFilePath = routeToRelPath(route, isAuth);
9066
- const manifest = await loadManifest5(projectRoot);
9166
+ const manifest = await loadManifest6(projectRoot);
9067
9167
  printPostGenerationReport({
9068
9168
  action: "updated",
9069
9169
  pageTitle: pageDef.name || pageDef.id || "Page",
@@ -9180,7 +9280,7 @@ function hasNavChanged(before, after) {
9180
9280
  import chalk12 from "chalk";
9181
9281
  import { resolve as resolve8 } from "path";
9182
9282
  import { existsSync as existsSync15, readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync5 } from "fs";
9183
- import { DesignSystemManager as DesignSystemManager6, ComponentManager as ComponentManager3, loadManifest as loadManifest6 } from "@getcoherent/core";
9283
+ import { DesignSystemManager as DesignSystemManager6, ComponentManager as ComponentManager4, loadManifest as loadManifest7 } from "@getcoherent/core";
9184
9284
  var DEBUG3 = process.env.COHERENT_DEBUG === "1";
9185
9285
  async function interactiveChat(options, chatCommandFn) {
9186
9286
  const { createInterface } = await import("readline");
@@ -9191,7 +9291,7 @@ async function interactiveChat(options, chatCommandFn) {
9191
9291
  const config2 = await loadConfig(configPath);
9192
9292
  const dsm = new DesignSystemManager6(configPath);
9193
9293
  await dsm.load();
9194
- const cm = new ComponentManager3(config2);
9294
+ const cm = new ComponentManager4(config2);
9195
9295
  const validProviders = ["claude", "openai", "auto"];
9196
9296
  const provider = (options.provider || "auto").toLowerCase();
9197
9297
  if (!validProviders.includes(provider)) {
@@ -9250,7 +9350,7 @@ async function interactiveChat(options, chatCommandFn) {
9250
9350
  return;
9251
9351
  }
9252
9352
  if (lower === "components" || lower === "list components" || lower.includes("what components")) {
9253
- const manifest = await loadManifest6(projectRoot);
9353
+ const manifest = await loadManifest7(projectRoot);
9254
9354
  if (manifest.shared.length === 0) {
9255
9355
  console.log(chalk12.gray("\n No shared components yet.\n"));
9256
9356
  } else {
@@ -9284,7 +9384,7 @@ async function interactiveChat(options, chatCommandFn) {
9284
9384
  }
9285
9385
  if (lower === "status") {
9286
9386
  const currentConfig = dsm.getConfig();
9287
- const manifest = await loadManifest6(projectRoot);
9387
+ const manifest = await loadManifest7(projectRoot);
9288
9388
  console.log(chalk12.bold(`
9289
9389
  ${currentConfig.name || "Coherent Project"}`));
9290
9390
  console.log(
@@ -9417,7 +9517,7 @@ async function chatCommand(message, options) {
9417
9517
  const storedHashes = await loadHashes(projectRoot);
9418
9518
  const dsm = new DesignSystemManager7(configPath);
9419
9519
  await dsm.load();
9420
- const cm = new ComponentManager4(config2);
9520
+ const cm = new ComponentManager5(config2);
9421
9521
  const pm = new PageManager3(config2, cm);
9422
9522
  spinner.succeed("Configuration loaded");
9423
9523
  message = await resolveTargetFlags(message, options, config2, projectRoot);
@@ -9473,7 +9573,7 @@ async function chatCommand(message, options) {
9473
9573
  }
9474
9574
  }
9475
9575
  spinner.start("Parsing your request...");
9476
- let manifest = await loadManifest7(project.root);
9576
+ let manifest = await loadManifest8(project.root);
9477
9577
  const validShared = manifest.shared.filter((s) => {
9478
9578
  const fp = resolve9(project.root, s.file);
9479
9579
  return existsSync16(fp);
@@ -9486,12 +9586,7 @@ async function chatCommand(message, options) {
9486
9586
  console.log(chalk13.dim(`[pre-gen] Cleaned ${cleaned} orphaned component(s) from manifest`));
9487
9587
  }
9488
9588
  }
9489
- const sharedComponentsSummary = manifest.shared.length > 0 ? manifest.shared.map((e) => {
9490
- const importPath = e.file.replace(/^components\/shared\//, "").replace(/\.tsx$/, "");
9491
- const desc = e.description ? ` \u2014 ${e.description}` : "";
9492
- return ` ${e.id} ${e.name} (${e.type})${desc}
9493
- Import: @/components/shared/${importPath}`;
9494
- }).join("\n") : void 0;
9589
+ const sharedComponentsSummary = buildSharedComponentsSummary(manifest);
9495
9590
  if (DEBUG4 && sharedComponentsSummary) {
9496
9591
  console.log(chalk13.dim("[add-page] sharedComponentsSummary in prompt:\n" + sharedComponentsSummary));
9497
9592
  }
@@ -10131,7 +10226,7 @@ import { DesignSystemManager as DesignSystemManager8, ComponentGenerator as Comp
10131
10226
  // src/utils/file-watcher.ts
10132
10227
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync18 } from "fs";
10133
10228
  import { relative as relative4, join as join13 } from "path";
10134
- import { loadManifest as loadManifest8, saveManifest as saveManifest3 } from "@getcoherent/core";
10229
+ import { loadManifest as loadManifest9, saveManifest as saveManifest3 } from "@getcoherent/core";
10135
10230
 
10136
10231
  // src/utils/component-integrity.ts
10137
10232
  import { existsSync as existsSync17, readFileSync as readFileSync11, readdirSync as readdirSync3 } from "fs";
@@ -10485,7 +10580,7 @@ async function handleFileChange(projectRoot, filePath) {
10485
10580
  if (config2.warnSharedReuse) {
10486
10581
  let manifest;
10487
10582
  try {
10488
- manifest = await loadManifest8(projectRoot);
10583
+ manifest = await loadManifest9(projectRoot);
10489
10584
  } catch {
10490
10585
  manifest = { shared: [], nextId: 1 };
10491
10586
  }
@@ -10504,7 +10599,7 @@ async function handleFileDelete(projectRoot, filePath) {
10504
10599
  if (!relativePath.startsWith("components/") || relativePath.startsWith("components/ui/")) return;
10505
10600
  try {
10506
10601
  const chalk33 = (await import("chalk")).default;
10507
- const manifest = await loadManifest8(projectRoot);
10602
+ const manifest = await loadManifest9(projectRoot);
10508
10603
  const orphaned = manifest.shared.find((s) => s.file === relativePath);
10509
10604
  if (orphaned) {
10510
10605
  const cleaned = {
@@ -10525,7 +10620,7 @@ async function detectNewComponent(projectRoot, filePath) {
10525
10620
  if (!relativePath.endsWith(".tsx") && !relativePath.endsWith(".jsx")) return;
10526
10621
  try {
10527
10622
  const chalk33 = (await import("chalk")).default;
10528
- const manifest = await loadManifest8(projectRoot);
10623
+ const manifest = await loadManifest9(projectRoot);
10529
10624
  const alreadyRegistered = manifest.shared.some((s) => s.file === relativePath);
10530
10625
  if (alreadyRegistered) return;
10531
10626
  const code = readFileSync12(filePath, "utf-8");
@@ -11449,10 +11544,10 @@ import { readdirSync as readdirSync5, readFileSync as readFileSync14, existsSync
11449
11544
  import { resolve as resolve12, join as join16 } from "path";
11450
11545
  import {
11451
11546
  DesignSystemManager as DesignSystemManager11,
11452
- ComponentManager as ComponentManager5,
11547
+ ComponentManager as ComponentManager6,
11453
11548
  PageManager as PageManager4,
11454
11549
  ComponentGenerator as ComponentGenerator4,
11455
- loadManifest as loadManifest9,
11550
+ loadManifest as loadManifest10,
11456
11551
  saveManifest as saveManifest4
11457
11552
  } from "@getcoherent/core";
11458
11553
  function extractComponentIdsFromCode2(code) {
@@ -11539,7 +11634,7 @@ async function fixCommand(opts = {}) {
11539
11634
  dsm = new DesignSystemManager11(project.configPath);
11540
11635
  await dsm.load();
11541
11636
  const config2 = dsm.getConfig();
11542
- cm = new ComponentManager5(config2);
11637
+ cm = new ComponentManager6(config2);
11543
11638
  pm = new PageManager4(config2, cm);
11544
11639
  const missingComponents = [];
11545
11640
  const missingFiles = [];
@@ -11662,7 +11757,7 @@ async function fixCommand(opts = {}) {
11662
11757
  fileIssues.push({ path: relativePath, report });
11663
11758
  }
11664
11759
  try {
11665
- let manifest = await loadManifest9(project.root);
11760
+ let manifest = await loadManifest10(project.root);
11666
11761
  let manifestModified = false;
11667
11762
  const { manifest: cleaned, removed: orphaned } = removeOrphanedEntries(project.root, manifest);
11668
11763
  if (orphaned.length > 0) {
@@ -11762,7 +11857,7 @@ async function fixCommand(opts = {}) {
11762
11857
  import chalk19 from "chalk";
11763
11858
  import { resolve as resolve13 } from "path";
11764
11859
  import { readdirSync as readdirSync6, readFileSync as readFileSync15, statSync as statSync2, existsSync as existsSync22 } from "fs";
11765
- import { loadManifest as loadManifest10 } from "@getcoherent/core";
11860
+ import { loadManifest as loadManifest11 } from "@getcoherent/core";
11766
11861
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", "design-system"]);
11767
11862
  function findTsxFiles(dir) {
11768
11863
  const results = [];
@@ -11889,7 +11984,7 @@ async function checkCommand(opts = {}) {
11889
11984
  \u{1F517} Internal Links`) + chalk19.dim(` \u2014 all ${result.links.total} links resolve \u2713`));
11890
11985
  }
11891
11986
  try {
11892
- const manifest = await loadManifest10(project.root);
11987
+ const manifest = await loadManifest11(project.root);
11893
11988
  if (manifest.shared.length > 0) {
11894
11989
  for (const entry of manifest.shared) {
11895
11990
  const fullPath = resolve13(project.root, entry.file);
@@ -11905,7 +12000,7 @@ async function checkCommand(opts = {}) {
11905
12000
  }
11906
12001
  if (!skipShared) {
11907
12002
  try {
11908
- const manifest = await loadManifest10(projectRoot);
12003
+ const manifest = await loadManifest11(projectRoot);
11909
12004
  if (!opts.json && manifest.shared.length > 0) {
11910
12005
  console.log(chalk19.cyan(`
11911
12006
  \u{1F9E9} Shared Components`) + chalk19.dim(` (${manifest.shared.length} registered)
@@ -12086,9 +12181,9 @@ import { Command } from "commander";
12086
12181
  import chalk25 from "chalk";
12087
12182
  import {
12088
12183
  DesignSystemManager as DesignSystemManager12,
12089
- ComponentManager as ComponentManager6,
12090
- loadManifest as loadManifest11,
12091
- generateSharedComponent as generateSharedComponent4,
12184
+ ComponentManager as ComponentManager7,
12185
+ loadManifest as loadManifest12,
12186
+ generateSharedComponent as generateSharedComponent5,
12092
12187
  integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout3
12093
12188
  } from "@getcoherent/core";
12094
12189
  import { existsSync as existsSync23 } from "fs";
@@ -12127,8 +12222,8 @@ function createComponentsCommand() {
12127
12222
  const dsm = new DesignSystemManager12(project.configPath);
12128
12223
  await dsm.load();
12129
12224
  const config2 = dsm.getConfig();
12130
- const cm = new ComponentManager6(config2);
12131
- const manifest = await loadManifest11(project.root);
12225
+ const cm = new ComponentManager7(config2);
12226
+ const manifest = await loadManifest12(project.root);
12132
12227
  if (opts.json) {
12133
12228
  const installed2 = cm.getAllComponents();
12134
12229
  console.log(JSON.stringify({ shared: manifest.shared, ui: installed2 }, null, 2));
@@ -12180,7 +12275,7 @@ function createComponentsCommand() {
12180
12275
  sharedCmd.option("--json", "Machine-readable JSON output").option("--verbose", "Show file paths and usage details").action(async (opts) => {
12181
12276
  const project = findConfig();
12182
12277
  if (!project) exitNotCoherent();
12183
- const manifest = await loadManifest11(project.root);
12278
+ const manifest = await loadManifest12(project.root);
12184
12279
  if (opts.json) {
12185
12280
  console.log(JSON.stringify(manifest, null, 2));
12186
12281
  return;
@@ -12209,7 +12304,7 @@ function createComponentsCommand() {
12209
12304
  const project = findConfig();
12210
12305
  if (!project) exitNotCoherent();
12211
12306
  const type = opts.type === "section" || opts.type === "widget" ? opts.type : "layout";
12212
- const result = await generateSharedComponent4(project.root, {
12307
+ const result = await generateSharedComponent5(project.root, {
12213
12308
  name: name.trim(),
12214
12309
  type,
12215
12310
  description: opts.description,
@@ -12260,7 +12355,7 @@ import {
12260
12355
  EXAMPLE_MULTIPAGE_CONFIG,
12261
12356
  normalizeFigmaComponents,
12262
12357
  setSharedMapping,
12263
- generateSharedComponent as generateSharedComponent5,
12358
+ generateSharedComponent as generateSharedComponent6,
12264
12359
  generatePagesFromFigma,
12265
12360
  integrateSharedLayoutIntoRootLayout as integrateSharedLayoutIntoRootLayout4,
12266
12361
  DesignSystemManager as DesignSystemManager13,
@@ -12457,7 +12552,7 @@ async function importFigmaAction(urlOrKey, opts) {
12457
12552
  else {
12458
12553
  stats.sharedCount++;
12459
12554
  if (!dryRun) {
12460
- const { id, name, file } = await generateSharedComponent5(projectRoot, {
12555
+ const { id, name, file } = await generateSharedComponent6(projectRoot, {
12461
12556
  name: entry.suggestedName,
12462
12557
  type: "widget",
12463
12558
  code: entry.suggestedTsx,
@@ -12884,7 +12979,7 @@ import { existsSync as existsSync26, readFileSync as readFileSync17 } from "fs";
12884
12979
  import { join as join20, relative as relative5, dirname as dirname10 } from "path";
12885
12980
  import { readdir as readdir4, readFile as readFile7 } from "fs/promises";
12886
12981
  import { DesignSystemManager as DesignSystemManager16 } from "@getcoherent/core";
12887
- import { loadManifest as loadManifest12, saveManifest as saveManifest5, findSharedComponent } from "@getcoherent/core";
12982
+ import { loadManifest as loadManifest13, saveManifest as saveManifest5, findSharedComponent } from "@getcoherent/core";
12888
12983
  function extractTokensFromProject(projectRoot) {
12889
12984
  const lightColors = {};
12890
12985
  const darkColors = {};
@@ -13164,7 +13259,7 @@ async function syncCommand(options = {}) {
13164
13259
  let reconcileResult = null;
13165
13260
  if (doComponents) {
13166
13261
  spinner.start("Reconciling shared components...");
13167
- const manifest = await loadManifest12(project.root);
13262
+ const manifest = await loadManifest13(project.root);
13168
13263
  const { manifest: reconciledManifest, result: rr } = reconcileComponents(project.root, manifest);
13169
13264
  reconcileResult = rr;
13170
13265
  if (!dryRun) {
@@ -316,6 +316,54 @@ Tasks:
316
316
  if (!content) throw new Error("Empty response from OpenAI");
317
317
  return content.trim().replace(/^```(?:tsx?|jsx?)\s*/i, "").replace(/\s*```$/i, "");
318
318
  }
319
+ async extractSharedComponents(pageCode, reservedNames, existingSharedNames) {
320
+ try {
321
+ const response = await this.client.chat.completions.create({
322
+ model: this.defaultModel,
323
+ messages: [
324
+ {
325
+ role: "system",
326
+ content: "You are a React/Next.js component extraction specialist. Analyze page code and identify reusable UI patterns that can be extracted into shared components. Return valid JSON only."
327
+ },
328
+ {
329
+ role: "user",
330
+ content: `Analyze this page and extract reusable components.
331
+
332
+ PAGE CODE:
333
+ ${pageCode}
334
+
335
+ Rules:
336
+ - Extract 1-5 components maximum
337
+ - Each component must be \u226510 lines of meaningful JSX
338
+ - Output complete, self-contained TypeScript modules with:
339
+ - "use client" directive (if hooks or event handlers are used)
340
+ - All necessary imports (shadcn/ui from @/components/ui/*, lucide-react, next/link, etc.)
341
+ - A typed props interface exported as a named type
342
+ - A named export function (not default export)
343
+ - Do NOT extract: the entire page, trivial wrappers, layout components (header, footer, nav)
344
+ - Do NOT use these names (reserved for shadcn/ui): ${reservedNames.join(", ")}
345
+ - Do NOT use these names (already shared): ${existingSharedNames.join(", ")}
346
+ - Look for: cards with icon+title+description, pricing tiers, testimonial blocks, stat displays, CTA sections
347
+
348
+ Each component object: "name" (PascalCase), "type" ("section"|"widget"), "description", "propsInterface", "code" (full TSX module as string)
349
+
350
+ If no repeating patterns found: { "components": [] }`
351
+ }
352
+ ],
353
+ response_format: { type: "json_object" },
354
+ temperature: 0.3,
355
+ max_tokens: 16384
356
+ });
357
+ const content = response.choices[0]?.message?.content;
358
+ if (!content) return { components: [] };
359
+ const jsonText = this.extractJSON(content);
360
+ const parsed = JSON.parse(jsonText);
361
+ const components = Array.isArray(parsed.components) ? parsed.components : [];
362
+ return { components };
363
+ } catch {
364
+ return { components: [] };
365
+ }
366
+ }
319
367
  async extractBlockAsComponent(pageCode, blockHint, componentName) {
320
368
  const response = await this.client.chat.completions.create({
321
369
  model: this.defaultModel,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.13",
6
+ "version": "0.5.14",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -33,15 +33,8 @@
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
- },
42
36
  "dependencies": {
43
37
  "@anthropic-ai/sdk": "^0.32.0",
44
- "@getcoherent/core": "workspace:*",
45
38
  "chalk": "^5.3.0",
46
39
  "chokidar": "^4.0.1",
47
40
  "commander": "^11.1.0",
@@ -49,12 +42,19 @@
49
42
  "open": "^10.1.0",
50
43
  "ora": "^7.0.1",
51
44
  "prompts": "^2.4.2",
52
- "zod": "^3.22.4"
45
+ "zod": "^3.22.4",
46
+ "@getcoherent/core": "0.5.14"
53
47
  },
54
48
  "devDependencies": {
55
49
  "@types/node": "^20.11.0",
56
50
  "@types/prompts": "^2.4.9",
57
51
  "tsup": "^8.0.1",
58
52
  "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
+ }