@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.
Files changed (2) hide show
  1. package/dist/index.js +84 -61
  2. 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
- WIDTH CONSISTENCY (CRITICAL \u2014 prevents visual mismatch between header and content):
4519
- - The shared Header and Footer use "mx-auto max-w-7xl px-4 sm:px-6 lg:px-8" for inner content.
4520
- - ALL page content MUST use the same width constraint: wrap the outermost element in <main className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">.
4521
- - Landing/marketing pages: each <section> is full-width for backgrounds, but inner content uses "mx-auto max-w-6xl" or similar.
4522
- - NEVER use max-w-4xl or smaller for the page wrapper \u2014 it makes the page look narrower than the header.
4523
- - For form-heavy pages (Settings, Create, Edit): the outer wrapper MUST still be max-w-7xl, but the form content inside can use max-w-4xl mx-auto for comfortable form width. The page title and description MUST remain at full max-w-7xl width.
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: main(mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6 flex flex-1 flex-col gap-4) > 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.
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
- return resolve5(projectRoot, "app", slug, "page.tsx");
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
- return `app/${slug}/page.tsx`;
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 enforceWidthWrapper(code, route) {
7145
- if (route === "/" || route === "/home") return { code, fixed: false };
7146
- if (/<section[\s>]/i.test(code) && (/<section[\s>]/gi.exec(code) || []).length >= 2) {
7147
- return { code, fixed: false };
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 didFix = false;
7151
- const mainMatch = result.match(/<main\s+className="([^"]*)"/);
7152
- if (mainMatch) {
7153
- const cls = mainMatch[1];
7154
- const needsMaxW = !cls.includes("max-w-7xl");
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, fixed: didFix };
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
- const { code: widthFixed, fixed: widthWasFixed } = enforceWidthWrapper(codeToWrite, route);
7567
- codeToWrite = widthFixed;
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
- const { code: widthFixed2, fixed: widthWasFixed2 } = enforceWidthWrapper(codeToWrite, route);
7737
- codeToWrite = widthFixed2;
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}`)));
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.11",
6
+ "version": "0.4.0",
7
7
  "description": "CLI interface for Coherent Design Method",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",