@getcoherent/cli 0.3.11 → 0.4.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/dist/index.js +84 -61
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2370,6 +2370,7 @@ export default config
|
|
|
2370
2370
|
}
|
|
2371
2371
|
await scaffolder.generateRootLayout();
|
|
2372
2372
|
await configureNextImages(projectPath);
|
|
2373
|
+
await createAppRouteGroupLayout(projectPath);
|
|
2373
2374
|
const welcomeMarkdown = getWelcomeMarkdown();
|
|
2374
2375
|
const homePageContent = generateWelcomeComponent(welcomeMarkdown);
|
|
2375
2376
|
await writeFile(join5(projectPath, "app", "page.tsx"), homePageContent);
|
|
@@ -2397,6 +2398,7 @@ export default config
|
|
|
2397
2398
|
console.log(chalk4.yellow(` Run manually: npm install ${COHERENT_REQUIRED_PACKAGES.join(" ")}`));
|
|
2398
2399
|
}
|
|
2399
2400
|
await ensureRegistryComponents(config2, projectPath);
|
|
2401
|
+
await createAppRouteGroupLayout(projectPath);
|
|
2400
2402
|
const designSystemSpinner = ora("Creating design system pages...").start();
|
|
2401
2403
|
await scaffolder.generateDesignSystemPages();
|
|
2402
2404
|
designSystemSpinner.succeed("Design system pages created");
|
|
@@ -2448,6 +2450,23 @@ export default nextConfig;
|
|
|
2448
2450
|
`;
|
|
2449
2451
|
await writeFile(configPath, content);
|
|
2450
2452
|
}
|
|
2453
|
+
async function createAppRouteGroupLayout(projectPath) {
|
|
2454
|
+
const dir = join5(projectPath, "app", "(app)");
|
|
2455
|
+
mkdirSync3(dir, { recursive: true });
|
|
2456
|
+
const layoutCode = `export default function AppLayout({
|
|
2457
|
+
children,
|
|
2458
|
+
}: {
|
|
2459
|
+
children: React.ReactNode
|
|
2460
|
+
}) {
|
|
2461
|
+
return (
|
|
2462
|
+
<main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
|
2463
|
+
{children}
|
|
2464
|
+
</main>
|
|
2465
|
+
)
|
|
2466
|
+
}
|
|
2467
|
+
`;
|
|
2468
|
+
await writeFile(join5(dir, "layout.tsx"), layoutCode);
|
|
2469
|
+
}
|
|
2451
2470
|
|
|
2452
2471
|
// src/commands/chat.ts
|
|
2453
2472
|
import chalk13 from "chalk";
|
|
@@ -4511,17 +4530,15 @@ LAYOUT CONTRACT (CRITICAL \u2014 prevents duplicate navigation and footer):
|
|
|
4511
4530
|
- The app has a root layout (app/layout.tsx) that renders a shared Header and Footer.
|
|
4512
4531
|
- Pages are rendered INSIDE this layout, between the Header and Footer.
|
|
4513
4532
|
- 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
4533
|
- 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
4534
|
- Do NOT add any navigation bars, logo headers, site-wide menus, or site footers to pages. The layout provides all of these.
|
|
4517
4535
|
|
|
4518
|
-
|
|
4519
|
-
-
|
|
4520
|
-
-
|
|
4521
|
-
-
|
|
4522
|
-
-
|
|
4523
|
-
-
|
|
4524
|
-
- NEVER have page content at full width while header/footer are constrained \u2014 this looks broken.
|
|
4536
|
+
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.
|
|
4541
|
+
- 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
4542
|
|
|
4526
4543
|
PAGE CONTENT (CRITICAL \u2014 prevents empty or duplicate pages):
|
|
4527
4544
|
- Every page MUST have substantial content. NEVER generate a page with only metadata and an empty <main> element.
|
|
@@ -4533,7 +4550,7 @@ pageCode rules (shadcn/ui blocks quality):
|
|
|
4533
4550
|
- Follow ALL design constraints above: text-sm base, semantic colors only, restricted spacing, weight-based hierarchy.
|
|
4534
4551
|
- 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
4552
|
- 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:
|
|
4553
|
+
- 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
4554
|
- No placeholders: real contextual copy only. Use the EXACT text, language, and content from the user's request.
|
|
4538
4555
|
- 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
4556
|
- 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 +5066,11 @@ import { resolve as resolve5 } from "path";
|
|
|
5049
5066
|
import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
|
|
5050
5067
|
import { DesignSystemManager as DesignSystemManager3, loadManifest as loadManifest4 } from "@getcoherent/core";
|
|
5051
5068
|
import chalk8 from "chalk";
|
|
5069
|
+
var MARKETING_ROUTES = /* @__PURE__ */ new Set(["", "landing", "pricing", "about", "contact", "blog", "features"]);
|
|
5070
|
+
function isMarketingRoute(route) {
|
|
5071
|
+
const slug = route.replace(/^\//, "").split("/")[0] || "";
|
|
5072
|
+
return MARKETING_ROUTES.has(slug);
|
|
5073
|
+
}
|
|
5052
5074
|
function routeToFsPath(projectRoot, route, isAuth) {
|
|
5053
5075
|
const slug = route.replace(/^\//, "");
|
|
5054
5076
|
if (isAuth) {
|
|
@@ -5057,7 +5079,10 @@ function routeToFsPath(projectRoot, route, isAuth) {
|
|
|
5057
5079
|
if (!slug) {
|
|
5058
5080
|
return resolve5(projectRoot, "app", "page.tsx");
|
|
5059
5081
|
}
|
|
5060
|
-
|
|
5082
|
+
if (isMarketingRoute(route)) {
|
|
5083
|
+
return resolve5(projectRoot, "app", slug, "page.tsx");
|
|
5084
|
+
}
|
|
5085
|
+
return resolve5(projectRoot, "app", "(app)", slug, "page.tsx");
|
|
5061
5086
|
}
|
|
5062
5087
|
function routeToRelPath(route, isAuth) {
|
|
5063
5088
|
const slug = route.replace(/^\//, "");
|
|
@@ -5067,7 +5092,10 @@ function routeToRelPath(route, isAuth) {
|
|
|
5067
5092
|
if (!slug) {
|
|
5068
5093
|
return "app/page.tsx";
|
|
5069
5094
|
}
|
|
5070
|
-
|
|
5095
|
+
if (isMarketingRoute(route)) {
|
|
5096
|
+
return `app/${slug}/page.tsx`;
|
|
5097
|
+
}
|
|
5098
|
+
return `app/(app)/${slug}/page.tsx`;
|
|
5071
5099
|
}
|
|
5072
5100
|
function deduplicatePages(pages) {
|
|
5073
5101
|
const normalize = (route) => route.replace(/\/$/, "").replace(/s$/, "").replace(/ue$/, "");
|
|
@@ -6802,12 +6830,32 @@ async function regenerateLayout(config2, projectRoot) {
|
|
|
6802
6830
|
try {
|
|
6803
6831
|
await integrateSharedLayoutIntoRootLayout2(projectRoot);
|
|
6804
6832
|
await ensureAuthRouteGroup(projectRoot);
|
|
6833
|
+
await ensureAppRouteGroupLayout(projectRoot);
|
|
6805
6834
|
} catch (err) {
|
|
6806
6835
|
if (process.env.COHERENT_DEBUG === "1") {
|
|
6807
6836
|
console.log(chalk9.dim("Layout integration warning:", err));
|
|
6808
6837
|
}
|
|
6809
6838
|
}
|
|
6810
6839
|
}
|
|
6840
|
+
async function ensureAppRouteGroupLayout(projectRoot) {
|
|
6841
|
+
const layoutPath = resolve6(projectRoot, "app", "(app)", "layout.tsx");
|
|
6842
|
+
if (existsSync14(layoutPath)) return;
|
|
6843
|
+
const { mkdir: mkdirAsync } = await import("fs/promises");
|
|
6844
|
+
await mkdirAsync(resolve6(projectRoot, "app", "(app)"), { recursive: true });
|
|
6845
|
+
const code = `export default function AppLayout({
|
|
6846
|
+
children,
|
|
6847
|
+
}: {
|
|
6848
|
+
children: React.ReactNode
|
|
6849
|
+
}) {
|
|
6850
|
+
return (
|
|
6851
|
+
<main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
|
6852
|
+
{children}
|
|
6853
|
+
</main>
|
|
6854
|
+
)
|
|
6855
|
+
}
|
|
6856
|
+
`;
|
|
6857
|
+
await writeFile(layoutPath, code);
|
|
6858
|
+
}
|
|
6811
6859
|
async function regenerateFiles(modified, config2, projectRoot) {
|
|
6812
6860
|
const componentIds = /* @__PURE__ */ new Set();
|
|
6813
6861
|
const pageIds = /* @__PURE__ */ new Set();
|
|
@@ -7141,53 +7189,26 @@ function stripInlineLayoutElements(code) {
|
|
|
7141
7189
|
}
|
|
7142
7190
|
return { code: result, stripped };
|
|
7143
7191
|
}
|
|
7144
|
-
function
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
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
|
+
};
|
|
7148
7203
|
}
|
|
7149
7204
|
let result = code;
|
|
7150
|
-
let
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
const needsMxAuto = !cls.includes("mx-auto");
|
|
7156
|
-
const needsPadding = !cls.includes("px-");
|
|
7157
|
-
if (needsMaxW || needsMxAuto || needsPadding) {
|
|
7158
|
-
let fixed = cls;
|
|
7159
|
-
fixed = fixed.replace(/\bmax-w-\S+/g, "");
|
|
7160
|
-
fixed = `${needsMxAuto ? "mx-auto " : ""}max-w-7xl ${needsPadding ? "px-4 sm:px-6 lg:px-8 " : ""}${fixed}`.replace(/\s+/g, " ").trim();
|
|
7161
|
-
result = result.replace(mainMatch[0], `<main className="${fixed}"`);
|
|
7162
|
-
didFix = true;
|
|
7163
|
-
}
|
|
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="[^"]*\bmx-auto\b[^"]*)\bmax-w-(xs|sm|md|lg|xl|2xl|3xl)\b/g,
|
|
7186
|
-
"$1max-w-4xl"
|
|
7187
|
-
);
|
|
7188
|
-
if (result !== before) didFix = true;
|
|
7205
|
+
let stripped = false;
|
|
7206
|
+
if (/<main\s+className="[^"]*">/.test(result)) {
|
|
7207
|
+
result = result.replace(/<main\s+className="[^"]*">\s*/g, '<div className="space-y-6">');
|
|
7208
|
+
result = result.replace(/<\/main>/g, "</div>");
|
|
7209
|
+
stripped = true;
|
|
7189
7210
|
}
|
|
7190
|
-
return { code: result,
|
|
7211
|
+
return { code: result, stripped };
|
|
7191
7212
|
}
|
|
7192
7213
|
async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider, originalMessage) {
|
|
7193
7214
|
switch (request.type) {
|
|
@@ -7563,11 +7584,12 @@ async function applyModification(request, dsm, cm, pm, projectRoot, aiProvider,
|
|
|
7563
7584
|
codeToWrite = autoFixed;
|
|
7564
7585
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7565
7586
|
codeToWrite = layoutStripped;
|
|
7566
|
-
|
|
7567
|
-
|
|
7587
|
+
if (!isMarketingRoute(route)) {
|
|
7588
|
+
const { code: noMain, stripped: mainStripped } = stripOuterMainWrapper(codeToWrite);
|
|
7589
|
+
if (mainStripped) codeToWrite = noMain;
|
|
7590
|
+
}
|
|
7568
7591
|
const allFixes = [...postFixes, ...autoFixes];
|
|
7569
7592
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
7570
|
-
if (widthWasFixed) allFixes.push("enforced max-w-7xl width consistency");
|
|
7571
7593
|
if (allFixes.length > 0) {
|
|
7572
7594
|
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7573
7595
|
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|
|
@@ -7733,11 +7755,12 @@ ${pagesCtx}`
|
|
|
7733
7755
|
codeToWrite = autoFixed;
|
|
7734
7756
|
const { code: layoutStripped, stripped } = stripInlineLayoutElements(codeToWrite);
|
|
7735
7757
|
codeToWrite = layoutStripped;
|
|
7736
|
-
|
|
7737
|
-
|
|
7758
|
+
if (!isMarketingRoute(route)) {
|
|
7759
|
+
const { code: noMain, stripped: mainStripped } = stripOuterMainWrapper(codeToWrite);
|
|
7760
|
+
if (mainStripped) codeToWrite = noMain;
|
|
7761
|
+
}
|
|
7738
7762
|
const allFixes = [...postFixes, ...autoFixes];
|
|
7739
7763
|
if (stripped.length > 0) allFixes.push(`stripped inline ${stripped.join(", ")} (layout owns these)`);
|
|
7740
|
-
if (widthWasFixed2) allFixes.push("enforced max-w-7xl width consistency");
|
|
7741
7764
|
if (allFixes.length > 0) {
|
|
7742
7765
|
console.log(chalk11.dim(" \u{1F527} Post-generation fixes:"));
|
|
7743
7766
|
allFixes.forEach((f) => console.log(chalk11.dim(` ${f}`)));
|