@getcoherent/cli 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.js +116 -84
  3. package/package.json +10 -10
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.
package/dist/index.js CHANGED
@@ -698,10 +698,7 @@ function generateWelcomeComponent(_markdown) {
698
698
  import { useState } from 'react'
699
699
  import {
700
700
  ArrowRight,
701
- Blocks,
702
701
  ClipboardCopy,
703
- Eye,
704
- Globe,
705
702
  LayoutDashboard,
706
703
  LogIn,
707
704
  Monitor,
@@ -709,7 +706,6 @@ import {
709
706
  Rocket,
710
707
  Settings,
711
708
  ShoppingBag,
712
- Sparkles,
713
709
  } from 'lucide-react'
714
710
 
715
711
  export default function HomePage() {
@@ -1020,47 +1016,7 @@ export default function HomePage() {
1020
1016
  </div>
1021
1017
  </section>
1022
1018
 
1023
- {/* Footer links (shared Footer component provides the main footer) */}
1024
- <div className="border-t pt-8 pb-8 mt-auto">
1025
- <div className="flex flex-col items-center gap-5 px-4">
1026
- <div className="flex items-center gap-3">
1027
- <div className="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shrink-0">
1028
- <Blocks className="size-4" />
1029
- </div>
1030
- <span className="text-sm font-semibold">Coherent Design Method</span>
1031
- </div>
1032
- <div className="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">
1033
- <a
1034
- href="https://getcoherent.design"
1035
- target="_blank"
1036
- rel="noopener noreferrer"
1037
- className="text-xs text-muted-foreground hover:text-foreground transition-colors"
1038
- >
1039
- getcoherent.design
1040
- </a>
1041
- <a href="https://github.com/skovtun/coherent-design-method" target="_blank" rel="noopener noreferrer" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1042
- GitHub
1043
- </a>
1044
- <a href="#" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1045
- Terms of Use
1046
- </a>
1047
- <a href="#" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
1048
- Privacy Policy
1049
- </a>
1050
- </div>
1051
- <p className="text-xs text-muted-foreground">
1052
- \xA9 {new Date().getFullYear()}{' '}
1053
- <a
1054
- href="https://www.linkedin.com/in/sergeikovtun/"
1055
- target="_blank"
1056
- rel="noopener noreferrer"
1057
- className="underline underline-offset-2 hover:text-foreground transition-colors"
1058
- >
1059
- Sergei Kovtun
1060
- </a>
1061
- </p>
1062
- </div>
1063
- </div>
1019
+
1064
1020
  </div>
1065
1021
  )
1066
1022
  }
@@ -2459,7 +2415,7 @@ async function createAppRouteGroupLayout(projectPath) {
2459
2415
  children: React.ReactNode
2460
2416
  }) {
2461
2417
  return (
2462
- <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
2418
+ <main className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
2463
2419
  {children}
2464
2420
  </main>
2465
2421
  )
@@ -4534,15 +4490,19 @@ LAYOUT CONTRACT (CRITICAL \u2014 prevents duplicate navigation and footer):
4534
4490
  - Do NOT add any navigation bars, logo headers, site-wide menus, or site footers to pages. The layout provides all of these.
4535
4491
 
4536
4492
  PAGE WRAPPER (CRITICAL \u2014 the layout provides width/padding automatically):
4537
- - App pages (dashboard, projects, team, settings, etc.) are rendered inside a route group layout that already provides <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">.
4538
- - Do NOT include any <main> wrapper, max-w-*, mx-auto, or px-* padding classes on the outermost element. The layout handles this.
4539
- - Start page content directly with a <div className="space-y-6"> or similar spacing wrapper. The first visible element should be the page title.
4540
- - Do NOT add inner centering wrappers like <div className="max-w-4xl mx-auto">. All content flows within the layout's max-w-7xl container.
4493
+ - App pages are rendered inside a route group layout that ALREADY provides: <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
4494
+ - Your outermost element MUST be exactly: <div className="space-y-6">
4495
+ - FORBIDDEN on the outermost element: <main>, max-w-*, mx-auto, px-*, py-*, p-*, flex-1, min-h-*
4496
+ - FORBIDDEN anywhere: <div className="max-w-4xl mx-auto">, <div className="max-w-2xl mx-auto">, or any inner centering wrapper
4497
+ - The first child inside <div className="space-y-6"> should be the page header (h1 + description)
4498
+ - ALL app pages must follow this exact same structure so content aligns consistently across pages
4541
4499
  - Landing/marketing pages are an exception: they render outside the app layout and should use full-width <section> elements with inner "mx-auto max-w-6xl" for content.
4542
4500
 
4543
4501
  PAGE CONTENT (CRITICAL \u2014 prevents empty or duplicate pages):
4544
4502
  - Every page MUST have substantial content. NEVER generate a page with only metadata and an empty <main> element.
4545
4503
  - NEVER create an inline preview/demo of another page (e.g., embedding a "dashboard view" inside the landing page with a toggle). Each page should be its own route.
4504
+ - NEVER create a single-page app (SPA) that renders multiple views via useState. Each view must be a separate Next.js page with its own route.
4505
+ - The home page (route "/") should be a simple redirect using next/navigation redirect('/dashboard') \u2014 OR a standalone landing page. NEVER a multi-view SPA.
4546
4506
  - Landing pages should link to app pages via <Link href="/dashboard">, NOT via useState toggles that render inline content.
4547
4507
 
4548
4508
  pageCode rules (shadcn/ui blocks quality):
@@ -6806,15 +6766,18 @@ async function regenerateLayout(config2, projectRoot) {
6806
6766
  const layoutPath = resolve6(projectRoot, "app", "layout.tsx");
6807
6767
  await writeFile(layoutPath, code);
6808
6768
  if (config2.navigation?.enabled && appType === "multi-page") {
6809
- const headerCode = generator.generateSharedHeaderCode();
6810
- await generateSharedComponent2(projectRoot, {
6811
- name: "Header",
6812
- type: "layout",
6813
- code: headerCode,
6814
- description: "Main site header with navigation and theme toggle",
6815
- usedIn: ["app/layout.tsx"],
6816
- overwrite: true
6817
- });
6769
+ const navType = config2.navigation.type || "header";
6770
+ if (navType === "header" || navType === "both") {
6771
+ const headerCode = generator.generateSharedHeaderCode();
6772
+ await generateSharedComponent2(projectRoot, {
6773
+ name: "Header",
6774
+ type: "layout",
6775
+ code: headerCode,
6776
+ description: "Main site header with navigation and theme toggle",
6777
+ usedIn: ["app/layout.tsx"],
6778
+ overwrite: true
6779
+ });
6780
+ }
6818
6781
  if (!hasSharedFooter) {
6819
6782
  const footerCode = generator.generateSharedFooterCode();
6820
6783
  await generateSharedComponent2(projectRoot, {
@@ -6826,35 +6789,69 @@ async function regenerateLayout(config2, projectRoot) {
6826
6789
  overwrite: true
6827
6790
  });
6828
6791
  }
6792
+ if (navType === "sidebar" || navType === "both") {
6793
+ const sidebarCode = generator.generateSharedSidebarCode();
6794
+ await generateSharedComponent2(projectRoot, {
6795
+ name: "Sidebar",
6796
+ type: "layout",
6797
+ code: sidebarCode,
6798
+ description: "Vertical sidebar navigation with collapsible sections",
6799
+ usedIn: ["app/(app)/layout.tsx"],
6800
+ overwrite: true
6801
+ });
6802
+ }
6829
6803
  }
6830
6804
  try {
6831
6805
  await integrateSharedLayoutIntoRootLayout2(projectRoot);
6832
6806
  await ensureAuthRouteGroup(projectRoot);
6833
- await ensureAppRouteGroupLayout(projectRoot);
6807
+ await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type);
6834
6808
  } catch (err) {
6835
6809
  if (process.env.COHERENT_DEBUG === "1") {
6836
6810
  console.log(chalk9.dim("Layout integration warning:", err));
6837
6811
  }
6838
6812
  }
6839
6813
  }
6840
- async function ensureAppRouteGroupLayout(projectRoot) {
6814
+ async function ensureAppRouteGroupLayout(projectRoot, navType) {
6841
6815
  const layoutPath = resolve6(projectRoot, "app", "(app)", "layout.tsx");
6842
6816
  if (existsSync14(layoutPath)) return;
6843
6817
  const { mkdir: mkdirAsync } = await import("fs/promises");
6844
6818
  await mkdirAsync(resolve6(projectRoot, "app", "(app)"), { recursive: true });
6845
- const code = `export default function AppLayout({
6819
+ const code = buildAppLayoutCode(navType);
6820
+ await writeFile(layoutPath, code);
6821
+ }
6822
+ function buildAppLayoutCode(navType) {
6823
+ const hasSidebar = navType === "sidebar" || navType === "both";
6824
+ if (hasSidebar) {
6825
+ return `import { Sidebar } from '@/components/shared/sidebar'
6826
+
6827
+ export default function AppLayout({
6846
6828
  children,
6847
6829
  }: {
6848
6830
  children: React.ReactNode
6849
6831
  }) {
6850
6832
  return (
6851
- <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
6833
+ <div className="flex min-h-[calc(100vh-3.5rem)]">
6834
+ <Sidebar />
6835
+ <main className="flex-1 px-4 sm:px-6 lg:px-8 py-6">
6836
+ {children}
6837
+ </main>
6838
+ </div>
6839
+ )
6840
+ }
6841
+ `;
6842
+ }
6843
+ return `export default function AppLayout({
6844
+ children,
6845
+ }: {
6846
+ children: React.ReactNode
6847
+ }) {
6848
+ return (
6849
+ <main className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
6852
6850
  {children}
6853
6851
  </main>
6854
6852
  )
6855
6853
  }
6856
6854
  `;
6857
- await writeFile(layoutPath, code);
6858
6855
  }
6859
6856
  async function regenerateFiles(modified, config2, projectRoot) {
6860
6857
  const componentIds = /* @__PURE__ */ new Set();
@@ -7189,26 +7186,41 @@ function stripInlineLayoutElements(code) {
7189
7186
  }
7190
7187
  return { code: result, stripped };
7191
7188
  }
7192
- function stripOuterMainWrapper(code) {
7193
- const mainWrapperRe = /(return\s*\(\s*\n?\s*)<main\s+className="[^"]*">\s*\n([\s\S]*?)\n\s*<\/main>(\s*\n?\s*\))/;
7194
- const match = code.match(mainWrapperRe);
7195
- if (match) {
7196
- const [, before, children, after] = match;
7197
- const dedented = children.replace(/^ /gm, " ");
7198
- return {
7199
- code: code.replace(match[0], `${before}<>${dedented}
7200
- </${">"}${after}`),
7201
- stripped: true
7202
- };
7189
+ var STANDARD_PAGE_WRAPPER = "space-y-6";
7190
+ var HOME_REDIRECT_CODE = `import { redirect } from 'next/navigation'
7191
+
7192
+ export default function Home() {
7193
+ redirect('/dashboard')
7194
+ }
7195
+ `;
7196
+ function detectAndFixSpaHomePage(code, route) {
7197
+ if (route !== "/" && route !== "") return { code, fixed: false };
7198
+ const hasMultipleRenders = (code.match(/const render\w+\s*=\s*\(\)/g) || []).length >= 2;
7199
+ const hasPageToggle = /useState\s*\(\s*['"](?:dashboard|home|page)/i.test(code);
7200
+ const isMassive = code.split("\n").length > 200;
7201
+ if (hasMultipleRenders && hasPageToggle || isMassive && hasPageToggle) {
7202
+ return { code: HOME_REDIRECT_CODE, fixed: true };
7203
7203
  }
7204
+ return { code, fixed: false };
7205
+ }
7206
+ function normalizePageWrapper(code) {
7204
7207
  let result = code;
7205
- let stripped = false;
7208
+ let fixed = false;
7206
7209
  if (/<main\s+className="[^"]*">/.test(result)) {
7207
- result = result.replace(/<main\s+className="[^"]*">\s*/g, '<div className="space-y-6">');
7210
+ result = result.replace(/<main\s+className="[^"]*">/g, `<div className="${STANDARD_PAGE_WRAPPER}">`);
7208
7211
  result = result.replace(/<\/main>/g, "</div>");
7209
- stripped = true;
7212
+ fixed = true;
7210
7213
  }
7211
- return { code: result, stripped };
7214
+ const outerDivRe = /(return\s*\(\s*\n?\s*)<div\s+className="([^"]*)">/;
7215
+ const match = result.match(outerDivRe);
7216
+ if (match) {
7217
+ const cls = match[2];
7218
+ if (cls !== STANDARD_PAGE_WRAPPER) {
7219
+ result = result.replace(match[0], `${match[1]}<div className="${STANDARD_PAGE_WRAPPER}">`);
7220
+ fixed = true;
7221
+ }
7222
+ }
7223
+ return { code: result, fixed };
7212
7224
  }
7213
7225
  async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider, originalMessage) {
7214
7226
  switch (request.type) {
@@ -7541,7 +7553,11 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7541
7553
  console.log(chalk11.yellow(`
7542
7554
  \u26A0\uFE0F Page "${page.name || page.id}" has no generated code \u2014 it will appear empty.`));
7543
7555
  console.log(chalk11.dim(" This usually means the AI did not produce pageCode for this page."));
7544
- console.log(chalk11.dim(' Try running: coherent chat "regenerate the ' + (page.name || page.id) + ' page with full content"'));
7556
+ console.log(
7557
+ chalk11.dim(
7558
+ ' Try running: coherent chat "regenerate the ' + (page.name || page.id) + ' page with full content"'
7559
+ )
7560
+ );
7545
7561
  }
7546
7562
  const pageForConfig = {
7547
7563
  ...page,
@@ -7582,11 +7598,19 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
7582
7598
  let codeToWrite = fixedCode;
7583
7599
  const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
7584
7600
  codeToWrite = autoFixed;
7601
+ const { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
7602
+ if (spaWasFixed) {
7603
+ codeToWrite = spaFixed;
7604
+ autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
7605
+ }
7585
7606
  const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
7586
7607
  codeToWrite = layoutStripped;
7587
7608
  if (!isMarketingRoute(route)) {
7588
- const { code: noMain, stripped: mainStripped } = stripOuterMainWrapper(codeToWrite);
7589
- if (mainStripped) codeToWrite = noMain;
7609
+ const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
7610
+ if (wrapperFixed) {
7611
+ codeToWrite = normalized;
7612
+ autoFixes.push("normalized page wrapper to standard spacing");
7613
+ }
7590
7614
  }
7591
7615
  const allFixes = [...postFixes, ...autoFixes];
7592
7616
  if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
@@ -7753,11 +7777,19 @@ ${pagesCtx}`
7753
7777
  let codeToWrite = fixedCode;
7754
7778
  const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
7755
7779
  codeToWrite = autoFixed;
7780
+ const { code: spaFixed, fixed: spaWasFixed } = detectAndFixSpaHomePage(codeToWrite, route);
7781
+ if (spaWasFixed) {
7782
+ codeToWrite = spaFixed;
7783
+ autoFixes.push("replaced SPA-style home page with redirect to /dashboard");
7784
+ }
7756
7785
  const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
7757
7786
  codeToWrite = layoutStripped;
7758
7787
  if (!isMarketingRoute(route)) {
7759
- const { code: noMain, stripped: mainStripped } = stripOuterMainWrapper(codeToWrite);
7760
- if (mainStripped) codeToWrite = noMain;
7788
+ const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
7789
+ if (wrapperFixed) {
7790
+ codeToWrite = normalized;
7791
+ autoFixes.push("normalized page wrapper to standard spacing");
7792
+ }
7761
7793
  }
7762
7794
  const allFixes = [...postFixes, ...autoFixes];
7763
7795
  if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
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.0"
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
+ }