create-odel-next
Version:
One-line starter for Next.js + shadcn + ODEL design system
772 lines (695 loc) • 25.9 kB
JavaScript
#!/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);
});