@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.
- package/LICENSE +21 -0
- package/dist/index.js +116 -84
- 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
|
-
|
|
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
|
|
4538
|
-
-
|
|
4539
|
-
-
|
|
4540
|
-
-
|
|
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
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
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 =
|
|
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
|
-
<
|
|
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
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
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
|
|
7208
|
+
let fixed = false;
|
|
7206
7209
|
if (/<main\s+className="[^"]*">/.test(result)) {
|
|
7207
|
-
result = result.replace(/<main\s+className="[^"]*"
|
|
7210
|
+
result = result.replace(/<main\s+className="[^"]*">/g, `<div className="${STANDARD_PAGE_WRAPPER}">`);
|
|
7208
7211
|
result = result.replace(/<\/main>/g, "</div>");
|
|
7209
|
-
|
|
7212
|
+
fixed = true;
|
|
7210
7213
|
}
|
|
7211
|
-
|
|
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(
|
|
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:
|
|
7589
|
-
if (
|
|
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:
|
|
7760
|
-
if (
|
|
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.
|
|
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
|
+
}
|