UNPKG

create-odel-next

Version:

One-line starter for Next.js + shadcn + ODEL design system

772 lines (695 loc) 25.9 kB
#!/usr/bin/env node import { execa } from "execa"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import enquirer from "enquirer"; const { Select } = enquirer; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Run a shell command and stream output. */ async function run(cmd, args, options = {}) { console.log(`\n> ${cmd} ${args.join(" ")}`); await execa(cmd, args, { stdio: "inherit", ...options }); } /** * Copy a folder recursively. */ function copyFolderRecursive(src, dest, options = {}) { if (!fs.existsSync(src)) { console.warn(`Template folder not found: ${src}`); return; } const skip = new Set(options.skip || []); if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } const entries = fs.readdirSync(src, { withFileTypes: true }); for (const entry of entries) { if (skip.has(entry.name)) continue; const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { copyFolderRecursive(srcPath, destPath, options); } else { fs.copyFileSync(srcPath, destPath); } } } /** * Interactive MCP client selection (arrow keys + Enter). */ async function chooseMcpClient() { const prompt = new Select({ name: "mcpClient", message: "Which MCP client do you want?", choices: [ { name: "cursor", message: "cursor" }, { name: "claude", message: "claude" }, { name: "vscode", message: "vscode" }, { name: "codex", message: "codex" }, { name: "skip", message: "skip MCP setup" } ], initial: 0 }); const result = await prompt.run(); if (result === "skip") return null; return result; } /** * Ask if this project should use the ODEL design system (colors/fonts/brand). */ async function chooseOdelPreset() { const prompt = new Select({ name: "odelPreset", message: "Apply ODEL design system (logos, fonts, colors)?", choices: [ { name: "yes", message: "Yes, this is an ODEL project" }, { name: "no", message: "No, keep default styling" } ], initial: 0 }); const result = await prompt.run(); return result === "yes"; } /** * Copy ODEL assets (brand logos, fonts, favicon) into the project's public/. * Expects odel/public/* inside the CLI repo. */ function copyOdelAssets(projectRoot) { const src = path.join(__dirname, "template", "odel", "public"); const dest = path.join(projectRoot, "public"); if (!fs.existsSync(src)) { console.warn("Warning: ODEL assets folder not found at template/odel/public."); return; } console.log("Copying ODEL logos, fonts, and favicon into project public/ ..."); copyFolderRecursive(src, dest); // Explicitly ensure favicon.svg is copied and replaces any existing one const faviconSrc = path.join(src, "favicon.svg"); const faviconDest = path.join(dest, "favicon.svg"); if (fs.existsSync(faviconSrc)) { fs.copyFileSync(faviconSrc, faviconDest); } // Also copy favicon to app directory (Next.js App Router supports favicon in app/) const appDirCandidates = [ path.join(projectRoot, "app"), path.join(projectRoot, "src", "app"), ]; const appDir = appDirCandidates.find((p) => fs.existsSync(p)); if (appDir && fs.existsSync(faviconSrc)) { const appFaviconDest = path.join(appDir, "favicon.svg"); fs.copyFileSync(faviconSrc, appFaviconDest); } } /** * Your custom components.json content that should replace the one created by shadcn. */ const COMPONENTS_JSON_CONTENT = `{ "$schema": "https://ui.shadcn.com/schema.json", "style": "new-york", "rsc": true, "tsx": true, "tailwind": { "config": "", "css": "app/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "registries": { "@8bitcn": "https://8bitcn.com/r/{name}.json", "@97cn": "https://97cn.itzik.co/r/{name}.json", "@aceternity": "https://ui.aceternity.com/registry/{name}.json", "@ai-elements": "https://registry.ai-sdk.dev/{name}.json", "@alexcarpenter": "https://ui.alexcarpenter.me/r/{name}.json", "@algolia": "https://sitesearch.algolia.com/r/{name}.json", "@alpine": "https://alpine-registry.vercel.app/r/{name}.json", "@aliimam": "https://aliimam.in/r/{name}.json", "@animate-ui": "https://animate-ui.com/r/{name}.json", "@assistant-ui": "https://r.assistant-ui.com/{name}.json", "@austin-ui": "https://austin-ui.netlify.app/r/{name}.json", "@basecn": "https://basecn.dev/r/{name}.json", "@better-upload": "https://better-upload.com/r/{name}.json", "@billingsdk": "https://billingsdk.com/r/{name}.json", "@blocks": "https://blocks.so/r/{name}.json", "@bucharitesh": "https://bucharitesh.in/r/{name}.json", "@clerk": "https://clerk.com/r/{name}.json", "@coss": "https://coss.com/ui/r/{name}.json", "@chisom-ui": "https://chisom-ui.netlify.app/r/{name}.json", "@creative-tim": "https://www.creative-tim.com/ui/r/{name}.json", "@cult-ui": "https://cult-ui.com/r/{name}.json", "@diceui": "https://diceui.com/r/{name}.json", "@efferd": "https://efferd.com/r/{name}.json", "@eldoraui": "https://eldoraui.site/r/{name}.json", "@elements": "https://tryelements.dev/r/{name}.json", "@elevenlabs-ui": "https://ui.elevenlabs.io/r/{name}.json", "@fancy": "https://fancycomponents.dev/r/{name}.json", "@formcn": "https://formcn.dev/r/{name}.json", "@heseui": "https://www.heseui.com/r/{name}.json", "@hooks": "https://shadcn-hooks.vercel.app/r/{name}.json", "@intentui": "https://intentui.com/r/{name}", "@kibo-ui": "https://www.kibo-ui.com/r/{name}.json", "@kanpeki": "https://kanpeki.vercel.app/r/{name}.json", "@kokonutui": "https://kokonutui.com/r/{name}.json", "@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json", "@lytenyte": "https://www.1771technologies.com/r/{name}.json", "@magicui": "https://magicui.design/r/{name}.json", "@magicui-pro": "https://pro.magicui.design/registry/{name}", "@motion-primitives": "https://motion-primitives.com/c/{name}.json", "@nativeui": "https://nativeui.io/registry/{name}.json", "@ncdai": "https://chanhdai.com/r/{name}.json", "@nuqs": "https://nuqs.dev/r/{name}.json", "@oui": "https://oui.mw10013.workers.dev/r/{name}.json", "@paceui": "https://ui.paceui.com/r/{name}.json", "@prompt-kit": "https://prompt-kit.com/c/{name}.json", "@prosekit": "https://prosekit.dev/r/{name}.json", "@react-bits": "https://reactbits.dev/r/{name}.json", "@react-market": "https://www.react-market.com/get/{name}.json", "@retroui": "https://retroui.dev/r/{name}.json", "@reui": "https://reui.io/r/{name}.json", "@rigidui": "https://rigidui.com/r/{name}.json", "@roiui": "https://roiui.com/r/{name}.json", "@solaceui": "https://www.solaceui.com/r/{name}.json", "@scrollxui": "https://www.scrollxui.dev/registry/{name}.json", "@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json", "@shadcn-map": "http://shadcn-map.vercel.app/r/{name}.json", "@shadcn-studio": "https://shadcnstudio.com/r/{name}.json", "@shadcnblocks": "https://shadcnblocks.com/r/{name}.json", "@simple-ai": "https://simple-ai.dev/r/{name}.json", "@skiper-ui": "https://skiper-ui.com/registry/{name}.json", "@skyr": "https://ui-play.skyroc.me/r/{name}.json", "@smoothui": "https://smoothui.dev/r/{name}.json", "@spectrumui": "https://ui.spectrumhq.in/r/{name}.json", "@supabase": "https://supabase.com/ui/r/{name}.json", "@svgl": "https://svgl.app/r/{name}.json", "@tailark": "https://tailark.com/r/{name}.json", "@tweakcn": "https://tweakcn.com/r/themes/{name}.json", "@wds": "https://wds-shadcn-registry.netlify.app/r/{name}.json", "@wandry-ui": "https://ui.wandry.com.ua/r/{name}.json", "@wigggle-ui": "https://wigggle-ui.vercel.app/r/{name}.json", "@paykit-sdk": "https://www.usepaykit.dev/r/{name}.json", "@pixelact-ui": "https://www.pixelactui.com/r/{name}.json", "@zippystarter": "https://zippystarter.com/r/{name}.json", "@shadcndesign": "https://shadcndesign-free.vercel.app/r/{name}.json", "@ha-components": "https://hacomponents.keshuac.com/r/{name}.json", "@shadix-ui": "https://shadix-ui.vercel.app/r/{name}.json", "@utilcn": "https://utilcn.dev/r/{name}.json" } }`; /** * Write/replace components.json in the project root with the custom config. */ function applyComponentsJson(projectRoot) { const target = path.join(projectRoot, "components.json"); fs.writeFileSync(target, COMPONENTS_JSON_CONTENT + "\n"); console.log(`components.json written to ${path.relative(projectRoot, target)}`); } /** * Create fonts.ts and layout.tsx to wire Ubuntu fonts automatically. */ function applyFontsSetup(projectRoot) { const appDirCandidates = [ path.join(projectRoot, "app"), path.join(projectRoot, "src", "app"), ]; const appDir = appDirCandidates.find((p) => fs.existsSync(p)); if (!appDir) { console.warn("Warning: Could not find app/ or src/app/ to apply fonts setup."); return; } const fontsPath = path.join(appDir, "fonts.ts"); const layoutPathTsx = path.join(appDir, "layout.tsx"); const layoutPathJsx = path.join(appDir, "layout.jsx"); const layoutPath = fs.existsSync(layoutPathTsx) ? layoutPathTsx : layoutPathJsx; const FONTS_TS_CONTENT = `import localFont from "next/font/local"; export const ubuntuEnglish = localFont({ src: [ { path: "../public/fonts/ubuntu-english/Ubuntu-Regular.ttf", weight: "400", style: "normal", }, { path: "../public/fonts/ubuntu-english/Ubuntu-Medium.ttf", weight: "500", style: "normal", }, { path: "../public/fonts/ubuntu-english/Ubuntu-Bold.ttf", weight: "700", style: "normal", }, ], variable: "--font-ubuntu-english", display: "swap", }); export const ubuntuArabic = localFont({ src: [ { path: "../public/fonts/ubuntu-arabic/Ubuntu-Arabic_R.ttf", weight: "400", style: "normal", }, { path: "../public/fonts/ubuntu-arabic/Ubuntu-Arabic_B.ttf", weight: "700", style: "normal", }, ], variable: "--font-ubuntu-arabic", display: "swap", }); export const ubuntuMono = localFont({ src: [ { path: "../public/fonts/ubuntu-english/UbuntuMono-R.ttf", weight: "400", style: "normal", }, { path: "../public/fonts/ubuntu-english/UbuntuMono-B.ttf", weight: "700", style: "normal", }, ], variable: "--font-ubuntu-mono", display: "swap", }); `; fs.writeFileSync(fontsPath, FONTS_TS_CONTENT); console.log(`fonts.ts written to ${path.relative(projectRoot, fontsPath)}`); const LAYOUT_CONTENT = `import type { Metadata } from "next"; import "./globals.css"; import { ubuntuEnglish, ubuntuArabic, ubuntuMono } from "./fonts"; export const metadata: Metadata = { title: "ODEL App", description: "ODEL Next.js starter", icons: { icon: "/favicon.svg", }, }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="en" className={\`\${ubuntuEnglish.variable} \${ubuntuArabic.variable} \${ubuntuMono.variable}\`} > <body className="font-sans antialiased">{children}</body> </html> ); } `; if (layoutPath) { fs.writeFileSync(layoutPath, LAYOUT_CONTENT); console.log(`layout file overwritten at ${path.relative(projectRoot, layoutPath)}`); } else { const defaultLayoutPath = layoutPathTsx; fs.writeFileSync(defaultLayoutPath, LAYOUT_CONTENT); console.log(`layout.tsx created at ${path.relative(projectRoot, defaultLayoutPath)}`); } } /** * Append ODEL CSS tokens to globals.css in app/ or src/app/. * Fonts here are wired to the variables defined in fonts.ts. */ function applyOdelPreset(projectRoot) { const candidates = [ path.join(projectRoot, "app", "globals.css"), path.join(projectRoot, "src", "app", "globals.css") ]; const globalsPath = candidates.find((p) => fs.existsSync(p)); if (!globalsPath) { console.warn("Warning: Could not find app/globals.css to apply ODEL styles."); return; } const odelCss = ` @import "tailwindcss"; @import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); /* Font-face declarations with unicode-range for automatic font selection */ /* Using a single font-family name - browser auto-selects based on character range */ @font-face { font-family: 'Ubuntu'; src: url('/fonts/ubuntu-arabic/Ubuntu-Arabic_R.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF; } @font-face { font-family: 'Ubuntu'; src: url('/fonts/ubuntu-arabic/Ubuntu-Arabic_B.ttf') format('truetype'); font-weight: 700; font-style: normal; font-display: swap; unicode-range: U+0600-06FF, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF; } @font-face { font-family: 'Ubuntu'; src: url('/fonts/ubuntu-english/Ubuntu-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Ubuntu'; src: url('/fonts/ubuntu-english/Ubuntu-Medium.ttf') format('truetype'); font-weight: 500; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @font-face { font-family: 'Ubuntu'; src: url('/fonts/ubuntu-english/Ubuntu-Bold.ttf') format('truetype'); font-weight: 700; font-style: normal; font-display: swap; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); --color-sidebar-ring: var(--sidebar-ring); --color-sidebar-border: var(--sidebar-border); --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-accent: var(--sidebar-accent); --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); --color-sidebar-primary: var(--sidebar-primary); --color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar: var(--sidebar); --color-chart-5: var(--chart-5); --color-chart-4: var(--chart-4); --color-chart-3: var(--chart-3); --color-chart-2: var(--chart-2); --color-chart-1: var(--chart-1); --color-ring: var(--ring); --color-input: var(--input); --color-border: var(--border); --color-destructive: var(--destructive); --color-accent-foreground: var(--accent-foreground); --color-accent: var(--accent); --color-muted-foreground: var(--muted-foreground); --color-muted: var(--muted); --color-secondary-foreground: var(--secondary-foreground); --color-secondary: var(--secondary); --color-primary-foreground: var(--primary-foreground); --color-primary: var(--primary); --color-popover-foreground: var(--popover-foreground); --color-popover: var(--popover); --color-card-foreground: var(--card-foreground); --color-card: var(--card); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); } :root { --radius: 0.625rem; --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: oklch(0.205 0 0); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); --ring: oklch(0.708 0 0); --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); --chart-4: oklch(0.828 0.189 84.429); --chart-5: oklch(0.769 0.188 70.08); --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); --sidebar-primary: oklch(0.205 0 0); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); --sidebar-ring: oklch(0.708 0 0); } .dark { --background: oklch(0.145 0 0); --foreground: oklch(0.985 0 0); --card: oklch(0.205 0 0); --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); --primary: oklch(0.922 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); --muted-foreground: oklch(0.708 0 0); --accent: oklch(0.269 0 0); --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); --ring: oklch(0.556 0 0); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); --chart-3: oklch(0.769 0.188 70.08); --chart-4: oklch(0.627 0.265 303.9); --chart-5: oklch(0.645 0.246 16.439); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); --sidebar-ring: oklch(0.556 0 0); } @layer base { * { @apply border-border outline-ring/50; } body { @apply bg-background text-foreground; font-family: var(--font-sans); } /* Ensure all text elements use the Ubuntu font */ h1, h2, h3, h4, h5, h6, p, span, div, a, button, input, textarea, select { font-family: var(--font-sans); } } /* ODEL design system tokens */ :root { --background: oklch(1 0 0); --foreground: oklch(0.4020 0.0104 271.1528); --card: oklch(1 0 0); --card-foreground: oklch(0.4020 0.0104 271.1528); --popover: oklch(1 0 0); --popover-foreground: oklch(0.4020 0.0104 271.1528); --primary: oklch(0.7346 0.1355 227.2440); --primary-foreground: oklch(0.9850 0 0); --secondary: oklch(0.9700 0 0); --secondary-foreground: oklch(0.6687 0.1565 241.2292); --muted: oklch(0.9700 0 0); --muted-foreground: oklch(0.5560 0 0); --accent: oklch(0.9700 0 0); --accent-foreground: oklch(0.6687 0.1565 241.2292); --destructive: oklch(0.5770 0.2450 27.3250); --destructive-foreground: oklch(1 0 0); --border: oklch(0.9220 0 0); --input: oklch(0.9220 0 0); --ring: oklch(0.7080 0 0); --chart-1: oklch(0.8100 0.1000 252); --chart-2: oklch(0.6200 0.1900 260); --chart-3: oklch(0.5500 0.2200 263); --chart-4: oklch(0.4900 0.2200 264); --chart-5: oklch(0.4200 0.1800 266); --sidebar: oklch(0.9850 0 0); --sidebar-foreground: oklch(0.4020 0.0104 271.1528); --sidebar-primary: oklch(0.4020 0.0104 271.1528); --sidebar-primary-foreground: oklch(0.9850 0 0); --sidebar-accent: oklch(0.9700 0 0); --sidebar-accent-foreground: oklch(0.4020 0.0104 271.1528); --sidebar-border: oklch(0.9220 0 0); --sidebar-ring: oklch(0.7080 0 0); --font-sans: 'Ubuntu', ui-sans-serif, system-ui, sans-serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: var(--font-ubuntu-mono); --radius: 0.625rem; --shadow-x: 0; --shadow-y: 1px; --shadow-blur: 3px; --shadow-spread: 0px; --shadow-opacity: 0.1; --shadow-color: oklch(0 0 0); --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); --tracking-normal: 0em; --spacing: 0.25rem; } .dark { --background: oklch(0.1870 0.0063 271.0667); --foreground: oklch(0.9850 0 0); --card: oklch(0.2050 0 0); --card-foreground: oklch(0.9850 0 0); --popover: oklch(0.2690 0 0); --popover-foreground: oklch(0.9850 0 0); --primary: oklch(0.7346 0.1355 227.2440); --primary-foreground: oklch(0.2050 0 0); --secondary: oklch(0.2690 0 0); --secondary-foreground: oklch(0.9850 0 0); --muted: oklch(0.2690 0 0); --muted-foreground: oklch(0.7080 0 0); --accent: oklch(0.3710 0 0); --accent-foreground: oklch(0.9850 0 0); --destructive: oklch(0.7040 0.1910 22.2160); --destructive-foreground: oklch(0.9850 0 0); --border: oklch(0.2750 0 0); --input: oklch(0.3250 0 0); --ring: oklch(0.5560 0 0); --chart-1: oklch(0.8100 0.1000 252); --chart-2: oklch(0.6200 0.1900 260); --chart-3: oklch(0.5500 0.2200 263); --chart-4: oklch(0.4900 0.2200 264); --chart-5: oklch(0.4200 0.1800 266); --sidebar: oklch(0.2050 0 0); --sidebar-foreground: oklch(0.9850 0 0); --sidebar-primary: oklch(0.4880 0.2430 264.3760); --sidebar-primary-foreground: oklch(0.9850 0 0); --sidebar-accent: oklch(0.2690 0 0); --sidebar-accent-foreground: oklch(0.9850 0 0); --sidebar-border: oklch(0.2750 0 0); --sidebar-ring: oklch(0.4390 0 0); --font-sans: 'Ubuntu', ui-sans-serif, system-ui, sans-serif; --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; --font-mono: var(--font-ubuntu-mono); --radius: 0.625rem; --shadow-x: 0; --shadow-y: 1px; --shadow-blur: 3px; --shadow-spread: 0px; --shadow-opacity: 0.1; --shadow-color: oklch(0 0 0); --shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); --shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05); --shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); --shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10); --shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10); --shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10); --shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10); --shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25); } `; fs.appendFileSync( globalsPath, "\n\n/* ODEL design system tokens */\n" + odelCss + "\n" ); console.log( `ODEL design system tokens appended to ${path.relative( projectRoot, globalsPath )}` ); } async function main() { const rawArg = process.argv[2]; if (!rawArg) { console.error("Error: Please provide a project name."); console.error("Example: npx create-odel-next my-app"); console.error("Or: npx create-odel-next ."); process.exit(1); } const cwd = process.cwd(); const isDot = rawArg === "." || rawArg === "./"; let finalProjectPath; let cnaWorkingDir = cwd; let cnaTargetArg = rawArg; if (isDot) { finalProjectPath = cwd; console.log("Using current directory as project root. create-next-app will validate it."); } else { finalProjectPath = path.resolve(cwd, rawArg); if (fs.existsSync(finalProjectPath)) { console.error(`Error: Folder "${rawArg}" already exists here.`); process.exit(1); } } console.log("Creating Next.js app..."); // create-next-app handles "." / "./" itself await run("npx", ["create-next-app@latest", cnaTargetArg], { cwd: cnaWorkingDir, }); process.chdir(finalProjectPath); console.log("\nInitializing shadcn/ui..."); try { await run("npx", ["shadcn@latest", "init"]); } catch (e) { console.warn("Warning: shadcn init failed. You may need to run it manually later."); } // Always override components.json after shadcn init applyComponentsJson(finalProjectPath); // ODEL preset const useOdel = await chooseOdelPreset(); if (useOdel) { copyOdelAssets(finalProjectPath); applyFontsSetup(finalProjectPath); applyOdelPreset(finalProjectPath); } else { console.log("Skipping ODEL design system preset."); } // MCP init const client = await chooseMcpClient(); if (client) { try { await run("npx", ["shadcn@latest", "mcp", "init", "--client", client]); } catch (e) { console.warn(`Warning: shadcn mcp init failed for client "${client}".`); } } else { console.log("Skipping MCP setup as requested."); } // .cursor template (optional) console.log("\nCopying .cursor template (if available)..."); const cursorTemplate = path.join(__dirname, "template", ".cursor"); const cursorTarget = path.join(finalProjectPath, ".cursor"); copyFolderRecursive(cursorTemplate, cursorTarget); console.log("\nDone."); console.log("Next steps:"); console.log(` cd ${isDot ? "." : path.basename(finalProjectPath)}`); console.log(" cursor . (or code .)"); console.log(" npm run dev"); } main().catch((err) => { console.error("Unexpected error:", err); process.exit(1); });