@getcoherent/cli 0.3.12 → 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 +170 -123
- 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
|
}
|
|
@@ -2370,6 +2326,7 @@ export default config
|
|
|
2370
2326
|
}
|
|
2371
2327
|
await scaffolder.generateRootLayout();
|
|
2372
2328
|
await configureNextImages(projectPath);
|
|
2329
|
+
await createAppRouteGroupLayout(projectPath);
|
|
2373
2330
|
const welcomeMarkdown = getWelcomeMarkdown();
|
|
2374
2331
|
const homePageContent = generateWelcomeComponent(welcomeMarkdown);
|
|
2375
2332
|
await writeFile(join5(projectPath, "app", "page.tsx"), homePageContent);
|
|
@@ -2397,6 +2354,7 @@ export default config
|
|
|
2397
2354
|
console.log(chalk4.yellow(` Run manually: npm install ${COHERENT_REQUIRED_PACKAGES.join(" ")}`));
|
|
2398
2355
|
}
|
|
2399
2356
|
await ensureRegistryComponents(config2, projectPath);
|
|
2357
|
+
await createAppRouteGroupLayout(projectPath);
|
|
2400
2358
|
const designSystemSpinner = ora("Creating design system pages...").start();
|
|
2401
2359
|
await scaffolder.generateDesignSystemPages();
|
|
2402
2360
|
designSystemSpinner.succeed("Design system pages created");
|
|
@@ -2448,6 +2406,23 @@ export default nextConfig;
|
|
|
2448
2406
|
`;
|
|
2449
2407
|
await writeFile(configPath, content);
|
|
2450
2408
|
}
|
|
2409
|
+
async function createAppRouteGroupLayout(projectPath) {
|
|
2410
|
+
const dir = join5(projectPath, "app", "(app)");
|
|
2411
|
+
mkdirSync3(dir, { recursive: true });
|
|
2412
|
+
const layoutCode = `export default function AppLayout({
|
|
2413
|
+
children,
|
|
2414
|
+
}: {
|
|
2415
|
+
children: React.ReactNode
|
|
2416
|
+
}) {
|
|
2417
|
+
return (
|
|
2418
|
+
<main className="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
|
2419
|
+
{children}
|
|
2420
|
+
</main>
|
|
2421
|
+
)
|
|
2422
|
+
}
|
|
2423
|
+
`;
|
|
2424
|
+
await writeFile(join5(dir, "layout.tsx"), layoutCode);
|
|
2425
|
+
}
|
|
2451
2426
|
|
|
2452
2427
|
// src/commands/chat.ts
|
|
2453
2428
|
import chalk13 from "chalk";
|
|
@@ -4511,21 +4486,23 @@ LAYOUT CONTRACT (CRITICAL \u2014 prevents duplicate navigation and footer):
|
|
|
4511
4486
|
- The app has a root layout (app/layout.tsx) that renders a shared Header and Footer.
|
|
4512
4487
|
- Pages are rendered INSIDE this layout, between the Header and Footer.
|
|
4513
4488
|
- NEVER include <header>, <nav>, or <footer> elements in pageCode. Also do NOT add a footer-like section at the bottom (no "\xA9 2024", no site links, no logo + nav links at the bottom).
|
|
4514
|
-
- Start page content with <main> or a wrapper <div>. The first visible element should be the page title or hero section.
|
|
4515
4489
|
- If the page needs sub-navigation (tabs, breadcrumbs, sidebar nav), use elements like <div role="tablist"> or <aside> \u2014 NOT <header>, <nav>, or <footer>.
|
|
4516
4490
|
- Do NOT add any navigation bars, logo headers, site-wide menus, or site footers to pages. The layout provides all of these.
|
|
4517
4491
|
|
|
4518
|
-
|
|
4519
|
-
-
|
|
4520
|
-
-
|
|
4521
|
-
-
|
|
4522
|
-
-
|
|
4523
|
-
-
|
|
4524
|
-
-
|
|
4492
|
+
PAGE WRAPPER (CRITICAL \u2014 the layout provides width/padding automatically):
|
|
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
|
|
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.
|
|
4525
4500
|
|
|
4526
4501
|
PAGE CONTENT (CRITICAL \u2014 prevents empty or duplicate pages):
|
|
4527
4502
|
- Every page MUST have substantial content. NEVER generate a page with only metadata and an empty <main> element.
|
|
4528
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.
|
|
4529
4506
|
- Landing pages should link to app pages via <Link href="/dashboard">, NOT via useState toggles that render inline content.
|
|
4530
4507
|
|
|
4531
4508
|
pageCode rules (shadcn/ui blocks quality):
|
|
@@ -4533,7 +4510,7 @@ pageCode rules (shadcn/ui blocks quality):
|
|
|
4533
4510
|
- Follow ALL design constraints above: text-sm base, semantic colors only, restricted spacing, weight-based hierarchy.
|
|
4534
4511
|
- Stat card pattern: Card > CardHeader(flex flex-row items-center justify-between space-y-0 pb-2) > CardTitle(text-sm font-medium) + Icon(size-4 text-muted-foreground) ; CardContent > metric(text-2xl font-bold) + change(text-xs text-muted-foreground).
|
|
4535
4512
|
- Login/form pattern: outer div(flex min-h-svh flex-col items-center justify-center p-6 md:p-10) > inner div(w-full max-w-sm) > Card with form.
|
|
4536
|
-
- Dashboard pattern:
|
|
4513
|
+
- Dashboard pattern: div(space-y-6) > page header(h1 text-2xl font-bold tracking-tight + p text-sm text-muted-foreground) > stats grid(grid gap-4 md:grid-cols-2 lg:grid-cols-4) > content cards. No <main> wrapper \u2014 the layout provides it.
|
|
4537
4514
|
- No placeholders: real contextual copy only. Use the EXACT text, language, and content from the user's request.
|
|
4538
4515
|
- IMAGES: For avatar/profile photos, use https://i.pravatar.cc/150?u=<unique-seed> (e.g. ?u=sarah.johnson). For hero/product images, use https://picsum.photos/800/400?random=N. Use standard <img> tags with className, NOT Next.js <Image>. Always provide alt text.
|
|
4539
4516
|
- BUTTON + LINK: The Button component supports asChild prop. To make a button that navigates, use <Button asChild><Link href="/path"><Plus className="size-4" /> Label</Link></Button>. Never nest <button> inside <Link> or vice versa without asChild.
|
|
@@ -5049,6 +5026,11 @@ import { resolve as resolve5 } from "path";
|
|
|
5049
5026
|
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
5050
5027
|
import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
|
|
5051
5028
|
import chalk8 from "chalk";
|
|
5029
|
+
var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
|
|
5030
|
+
function isMarketingRoute(route) {
|
|
5031
|
+
const slug = route.replace(/^\//, "").split("/")[0] || "";
|
|
5032
|
+
return MARKETING_ROUTES.has(slug);
|
|
5033
|
+
}
|
|
5052
5034
|
function routeToFsPath(projectRoot, route, isAuth) {
|
|
5053
5035
|
const slug = route.replace(/^\//, "");
|
|
5054
5036
|
if (isAuth) {
|
|
@@ -5057,7 +5039,10 @@ function routeToFsPath(projectRoot, route, isAuth) {
|
|
|
5057
5039
|
if (!slug) {
|
|
5058
5040
|
return resolve5(projectRoot, "app", "page.tsx");
|
|
5059
5041
|
}
|
|
5060
|
-
|
|
5042
|
+
if (isMarketingRoute(route)) {
|
|
5043
|
+
return resolve5(projectRoot, "app", slug, "page.tsx");
|
|
5044
|
+
}
|
|
5045
|
+
return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
|
|
5061
5046
|
}
|
|
5062
5047
|
function routeToRelPath(route, isAuth) {
|
|
5063
5048
|
const slug = route.replace(/^\//, "");
|
|
@@ -5067,7 +5052,10 @@ function routeToRelPath(route, isAuth) {
|
|
|
5067
5052
|
if (!slug) {
|
|
5068
5053
|
return "app/page.tsx";
|
|
5069
5054
|
}
|
|
5070
|
-
|
|
5055
|
+
if (isMarketingRoute(route)) {
|
|
5056
|
+
return `app/${slug}/page.tsx`;
|
|
5057
|
+
}
|
|
5058
|
+
return `app/(app)/${slug}/page.tsx`;
|
|
5071
5059
|
}
|
|
5072
5060
|
function deduplicatePages(pages) {
|
|
5073
5061
|
const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
|
|
@@ -6778,15 +6766,18 @@ async function regenerateLayout(config2, projectRoot) {
|
|
|
6778
6766
|
const layoutPath = resolve6(projectRoot, "app", "layout.tsx");
|
|
6779
6767
|
await writeFile(layoutPath, code);
|
|
6780
6768
|
if (config2.navigation?.enabled && appType === "multi-page") {
|
|
6781
|
-
const
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
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
|
+
}
|
|
6790
6781
|
if (!hasSharedFooter) {
|
|
6791
6782
|
const footerCode = generator.generateSharedFooterCode();
|
|
6792
6783
|
await generateSharedComponent2(projectRoot, {
|
|
@@ -6798,16 +6789,70 @@ async function regenerateLayout(config2, projectRoot) {
|
|
|
6798
6789
|
overwrite: true
|
|
6799
6790
|
});
|
|
6800
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
|
+
}
|
|
6801
6803
|
}
|
|
6802
6804
|
try {
|
|
6803
6805
|
await integrateSharedLayoutIntoRootLayout2(projectRoot);
|
|
6804
6806
|
await ensureAuthRouteGroup(projectRoot);
|
|
6807
|
+
await ensureAppRouteGroupLayout(projectRoot, config2.navigation?.type);
|
|
6805
6808
|
} catch (err) {
|
|
6806
6809
|
if (process.env.COHERENT_DEBUG === "1") {
|
|
6807
6810
|
console.log(chalk9.dim("Layout integration warning:", err));
|
|
6808
6811
|
}
|
|
6809
6812
|
}
|
|
6810
6813
|
}
|
|
6814
|
+
async function ensureAppRouteGroupLayout(projectRoot, navType) {
|
|
6815
|
+
const layoutPath = resolve6(projectRoot, "app", "(app)", "layout.tsx");
|
|
6816
|
+
if (existsSync14(layoutPath)) return;
|
|
6817
|
+
const { mkdir: mkdirAsync } = await import("fs/promises");
|
|
6818
|
+
await mkdirAsync(resolve6(projectRoot, "app", "(app)"), { recursive: true });
|
|
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({
|
|
6828
|
+
children,
|
|
6829
|
+
}: {
|
|
6830
|
+
children: React.ReactNode
|
|
6831
|
+
}) {
|
|
6832
|
+
return (
|
|
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">
|
|
6850
|
+
{children}
|
|
6851
|
+
</main>
|
|
6852
|
+
)
|
|
6853
|
+
}
|
|
6854
|
+
`;
|
|
6855
|
+
}
|
|
6811
6856
|
async function regenerateFiles(modified, config2, projectRoot) {
|
|
6812
6857
|
const componentIds = /* @__PURE__ */ new Set();
|
|
6813
6858
|
const pageIds = /* @__PURE__ */ new Set();
|
|
@@ -7141,61 +7186,41 @@ function stripInlineLayoutElements(code) {
|
|
|
7141
7186
|
}
|
|
7142
7187
|
return { code: result, stripped };
|
|
7143
7188
|
}
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
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 };
|
|
7148
7203
|
}
|
|
7204
|
+
return { code, fixed: false };
|
|
7205
|
+
}
|
|
7206
|
+
function normalizePageWrapper(code) {
|
|
7149
7207
|
let result = code;
|
|
7150
|
-
let
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
result = result.replace(
|
|
7162
|
-
|
|
7208
|
+
let fixed = false;
|
|
7209
|
+
if (/<main\s+className="[^"]*">/.test(result)) {
|
|
7210
|
+
result = result.replace(/<main\s+className="[^"]*">/g, `<div className="${STANDARD_PAGE_WRAPPER}">`);
|
|
7211
|
+
result = result.replace(/<\/main>/g, "</div>");
|
|
7212
|
+
fixed = true;
|
|
7213
|
+
}
|
|
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;
|
|
7163
7221
|
}
|
|
7164
|
-
} else {
|
|
7165
|
-
const returnMatch = result.match(/return\s*\(\s*\n?\s*<(div|main)\s+className="([^"]*)"/);
|
|
7166
|
-
if (returnMatch) {
|
|
7167
|
-
const [fullMatch, tag, cls] = returnMatch;
|
|
7168
|
-
const needsMaxW = !cls.includes("max-w-7xl");
|
|
7169
|
-
const needsMxAuto = !cls.includes("mx-auto");
|
|
7170
|
-
const needsPadding = !cls.includes("px-");
|
|
7171
|
-
if (needsMaxW || needsMxAuto || needsPadding) {
|
|
7172
|
-
let fixed = cls;
|
|
7173
|
-
fixed = fixed.replace(/\bmax-w-\S+/g, "");
|
|
7174
|
-
fixed = `${needsMxAuto ? "mx-auto " : ""}max-w-7xl ${needsPadding ? "px-4 sm:px-6 lg:px-8 " : ""}${fixed}`.replace(/\s+/g, " ").trim();
|
|
7175
|
-
result = result.replace(fullMatch, `return (
|
|
7176
|
-
<${tag} className="${fixed}"`);
|
|
7177
|
-
didFix = true;
|
|
7178
|
-
}
|
|
7179
|
-
}
|
|
7180
|
-
}
|
|
7181
|
-
const isAuthPage = /\/(login|signin|sign-in|signup|sign-up|register|auth)\b/i.test(route);
|
|
7182
|
-
if (!isAuthPage) {
|
|
7183
|
-
const before = result;
|
|
7184
|
-
result = result.replace(
|
|
7185
|
-
/(<div\s+className="[^"]*)\bmax-w-(xs|sm|md|lg|xl|2xl|3xl|4xl|5xl|6xl)\b/g,
|
|
7186
|
-
"$1"
|
|
7187
|
-
);
|
|
7188
|
-
result = result.replace(
|
|
7189
|
-
/(<div\s+className="[^"]*)\bmx-auto\b/g,
|
|
7190
|
-
"$1"
|
|
7191
|
-
);
|
|
7192
|
-
result = result.replace(
|
|
7193
|
-
/className="([^"]*)"/g,
|
|
7194
|
-
(_, classes) => `className="${classes.replace(/\s{2,}/g, " ").trim()}"`
|
|
7195
|
-
);
|
|
7196
|
-
if (result !== before) didFix = true;
|
|
7197
7222
|
}
|
|
7198
|
-
return { code: result, fixed
|
|
7223
|
+
return { code: result, fixed };
|
|
7199
7224
|
}
|
|
7200
7225
|
async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider, originalMessage) {
|
|
7201
7226
|
switch (request.type) {
|
|
@@ -7528,7 +7553,11 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7528
7553
|
console.log(chalk11.yellow(`
|
|
7529
7554
|
\u26A0\uFE0F Page "${page.name || page.id}" has no generated code \u2014 it will appear empty.`));
|
|
7530
7555
|
console.log(chalk11.dim(" This usually means the AI did not produce pageCode for this page."));
|
|
7531
|
-
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
|
+
);
|
|
7532
7561
|
}
|
|
7533
7562
|
const pageForConfig = {
|
|
7534
7563
|
...page,
|
|
@@ -7569,13 +7598,22 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7569
7598
|
let codeToWrite = fixedCode;
|
|
7570
7599
|
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
|
|
7571
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
|
+
}
|
|
7572
7606
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7573
7607
|
codeToWrite = layoutStripped;
|
|
7574
|
-
|
|
7575
|
-
|
|
7608
|
+
if (!isMarketingRoute(route)) {
|
|
7609
|
+
const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
|
|
7610
|
+
if (wrapperFixed) {
|
|
7611
|
+
codeToWrite = normalized;
|
|
7612
|
+
autoFixes.push("normalized page wrapper to standard spacing");
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7576
7615
|
const allFixes = [...postFixes, ...autoFixes];
|
|
7577
7616
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
7578
|
-
if (widthWasFixed) allFixes.push("enforced max-w-7xl width consistency");
|
|
7579
7617
|
if (allFixes.length > 0) {
|
|
7580
7618
|
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7581
7619
|
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
@@ -7739,13 +7777,22 @@ ${pagesCtx}`
|
|
|
7739
7777
|
let codeToWrite = fixedCode;
|
|
7740
7778
|
const { code: autoFixed, fixes: autoFixes } = await autoFixCode(codeToWrite);
|
|
7741
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
|
+
}
|
|
7742
7785
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7743
7786
|
codeToWrite = layoutStripped;
|
|
7744
|
-
|
|
7745
|
-
|
|
7787
|
+
if (!isMarketingRoute(route)) {
|
|
7788
|
+
const { code: normalized, fixed: wrapperFixed } = normalizePageWrapper(codeToWrite);
|
|
7789
|
+
if (wrapperFixed) {
|
|
7790
|
+
codeToWrite = normalized;
|
|
7791
|
+
autoFixes.push("normalized page wrapper to standard spacing");
|
|
7792
|
+
}
|
|
7793
|
+
}
|
|
7746
7794
|
const allFixes = [...postFixes, ...autoFixes];
|
|
7747
7795
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
7748
|
-
if (widthWasFixed2) allFixes.push("enforced max-w-7xl width consistency");
|
|
7749
7796
|
if (allFixes.length > 0) {
|
|
7750
7797
|
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7751
7798
|
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
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
|
+
}
|