UNPKG

create-esyt

Version:

A tool to create a new esyt project with your preferred stack

1,002 lines (966 loc) 36.4 kB
#!/usr/bin/env node import * as p from "@clack/prompts"; import fs from "fs"; import path from "path"; import { execSync } from "child_process"; // Import helper functions from lib/flags.js const flagsModule = await import("./lib/flags.js"); const { parseFlags, mapNpmToLabel, printHelp, validateFlags } = flagsModule; function detectPackageManager() { const userAgent = process.env.npm_config_user_agent || ""; const isBun = /bun/i.test(userAgent) || process.env.BUN || (process.versions && process.versions.bun); const isPnpm = /pnpm\//i.test(userAgent) || (process.env.npm_execpath || "").includes("pnpm"); if (isBun) { return { name: "bun", installCmd: "bun install", addCmd: "bun add", addDevCmd: "bun add -d", dlxCmd: "bunx", createViteCmd(projectName, template) { return `bun create vite@latest ${projectName} --template ${template}`; }, createNextCmd(projectName, flags) { return `bunx create-next-app@latest ${projectName} ${flags}`; }, }; } if (isPnpm) { return { name: "pnpm", installCmd: "pnpm i", addCmd: "pnpm add", addDevCmd: "pnpm add -D", dlxCmd: "pnpm dlx", createViteCmd(projectName, template) { return `pnpm create vite@latest ${projectName} --template ${template}`; }, createNextCmd(projectName, flags) { return `pnpm create next-app@latest ${projectName} ${flags}`; }, }; } return { name: "npm", installCmd: "npm i", addCmd: "npm i", addDevCmd: "npm i -D", dlxCmd: "npx", createViteCmd(projectName, template) { return `npm create vite@latest ${projectName} -- --template ${template}`; }, createNextCmd(projectName, flags) { return `npx create-next-app@latest ${projectName} ${flags}`; }, }; } function printTitle() { console.log(` ______ ______ __ __ ______ /\\ ___\\ /\\ ___\\ /\\ \\_\\ \\ /\\__ _\\ \\ \\ __\\ \\ \\___ \\ \\ \\____ \\ \\/_/\\ \\/ \\ \\_____\\ \\/\\_____\\ \\/\\_____\\ \\ \\_\\ \\/_____/ \\/_____/ \\/_____/ \\/_/ `); } // flag parsing helpers are provided by ./lib/flags.js async function run() { try { printTitle(); const pm = detectPackageManager(); // Parse CLI flags and use them to skip prompts when valid. We still validate // flags and fall back to interactive prompts for anything missing or wrong. const flags = parseFlags(process.argv.slice(2)); // Validate flags and clear any invalid values so we prompt only for those const invalid = validateFlags(flags); for (const k of Object.keys(invalid)) { // clear invalid keys so interactive prompts run for them flags[k] = undefined; } // Helper to run or print commands depending on dryRun/yes flags const runCommand = (cmd, options = {}) => { if (flags.dryRun) { console.log(`[dry-run] ${cmd}`); return; } return execSync(cmd, options); }; // If user provided --yes, fill sensible defaults for any missing values so // the CLI runs non-interactively. if (flags.yes) { flags.framework = flags.framework || "Vite"; flags.language = flags.language || "JavaScript"; flags.projectName = flags.projectName || "esyt-app"; flags.npmPackages = flags.npmPackages || []; flags.gitInit = typeof flags.gitInit === "boolean" ? flags.gitInit : true; flags.installDeps = typeof flags.installDeps === "boolean" ? flags.installDeps : true; flags.selectedIDE = flags.selectedIDE || "None"; // runDevServer default only makes sense if installDeps is true flags.runDevServer = typeof flags.runDevServer === "boolean" ? flags.runDevServer : true; } if (flags.help) { printHelp(); process.exit(0); } if (flags.version) { try { const pkg = JSON.parse( fs.readFileSync(path.join(process.cwd(), "package.json"), "utf8") ); console.log(pkg.version || ""); } catch (e) { console.log(""); } process.exit(0); } // 1. Framework selection let framework = flags.framework; if (!framework) { framework = await p.select({ message: "Which framework would you like to use?", options: [ { value: "Vite", label: "Vite" }, { value: "Next.js", label: "Next.js" }, ], }); if (p.isCancel(framework)) { p.cancel("Operation cancelled."); process.exit(0); } } // 2. Language selection let language = flags.language; if (!language) { language = await p.select({ message: "Will you be using JavaScript or TypeScript?", options: [ { value: "JavaScript", label: "JavaScript" }, { value: "TypeScript", label: "TypeScript" }, ], }); if (p.isCancel(language)) { p.cancel("Operation cancelled."); process.exit(0); } } // 3. Project name let projectName = flags.projectName; // Validate flag-provided project name: reject if contains spaces if (projectName && /\s/.test(projectName)) { console.log( "Provided project name contains spaces and will be ignored. Please enter a valid name." ); projectName = undefined; } if (!projectName) { projectName = await p.text({ message: "What will your project be called?", placeholder: "esyt-app", validate: (value) => { if (!value || value.trim() === "") { return "Project name is required."; } if (/\s/.test(value)) { return "Project name cannot contain spaces. Please use dashes or underscores."; } }, }); if (p.isCancel(projectName)) { p.cancel("Operation cancelled."); process.exit(0); } } // 4. Package selection prompt (contextual) let packageChoices = []; if (framework === "Vite") { packageChoices = [ "TailwindCSS", "React Router", "React Icons", "Framer Motion", "OGL", "DotENV", "Axios", "Firebase", "Clerk", "Appwrite", "Prisma", ]; } else if (framework === "Next.js") { packageChoices = [ "TailwindCSS", "React Icons", "Framer Motion", "DotENV", "Axios", "Firebase", "Clerk", "Appwrite", "Prisma", "next-auth", "@next/font", "next-seo", "next-sitemap", "next-pwa", ]; } // 4. Package selection. Merge interactive labels with flags.npmPackages. let packages = []; // Map flag-provided npm packages to the interactive labels when possible. if (flags.npmPackages && flags.npmPackages.length > 0) { for (const np of flags.npmPackages) { const mapped = mapNpmToLabel(np); if (mapped) packages.push(mapped); else { // keep the raw npm package name to install later packages.push(np); } } } // If no packages were provided via flags, prompt interactively (unless --yes) if (packages.length === 0) { if (flags.yes) { packages = []; } else { const selected = await p.multiselect({ message: "Which packages would you like to enable?", options: packageChoices.map((choice) => ({ value: choice, label: choice, })), }); if (p.isCancel(selected)) { p.cancel("Operation cancelled."); process.exit(0); } packages = selected; } } // 5. Git init, npm i, dev server, IDE selection prompts // 5. Git init, install, and IDE selection. Use flags when available. let gitInit = typeof flags.gitInit === "boolean" ? flags.gitInit : undefined; let installDeps = typeof flags.installDeps === "boolean" ? flags.installDeps : undefined; let selectedIDE = typeof flags.selectedIDE === "string" ? flags.selectedIDE : undefined; if (gitInit === undefined || installDeps === undefined || !selectedIDE) { if (gitInit === undefined) { const result = await p.confirm({ message: "Initialize a new git repository?", initialValue: false, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } gitInit = result; } if (installDeps === undefined) { const result = await p.confirm({ message: `Would you like us to run '${pm.installCmd}'?`, initialValue: true, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } installDeps = result; } if (!selectedIDE) { const result = await p.select({ message: "Which IDE would you like to open your project with?", options: [ { value: "Nvim", label: "Neovim" }, { value: "Zed", label: "Zed" }, { value: "VSCode", label: "VSCode" }, { value: "Cursor", label: "Cursor" }, { value: "Trae", label: "Trae" }, { value: "None", label: "None" }, ], initialValue: "None", }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } selectedIDE = result; } } let runDevServer = typeof flags.runDevServer === "boolean" ? flags.runDevServer : undefined; if (runDevServer === undefined && installDeps) { const result = await p.confirm({ message: "Would you like to run the development server automatically after setup?", initialValue: true, }); if (p.isCancel(result)) { p.cancel("Operation cancelled."); process.exit(0); } runDevServer = result; } const projectPath = path.join(process.cwd(), projectName); // --- Framework-specific project creation --- if (framework === "Vite") { const template = language === "JavaScript" ? "react" : "react-ts"; const viteCommand = pm.createViteCmd(projectName, template); console.log(`Running: ${viteCommand}`); // Newer versions of the Vite create flow ask interactive questions // (e.g. "Use rolldown-vite (Experimental)?" and "Install with bun and start now?"). // Auto-answer these prompts with "no" by sending two "no\n" lines to stdin. // We keep stdout/stderr inherited so the user still sees the command output. try { runCommand(viteCommand, { stdio: ["pipe", "inherit", "inherit"], input: Buffer.from("no\nno\n"), }); } catch (err) { throw err; } process.chdir(projectPath); try { if (installDeps) { runCommand(pm.installCmd, { stdio: "inherit" }); } } catch (error) { console.error("Error during initial setup:", error.message); } try { const filesToRemove = [ { path: path.join(projectPath, "src", "assets"), isDir: true, }, { path: path.join(projectPath, "README.md"), isDir: false, }, { path: path.join(projectPath, "public", "vite.svg"), isDir: false, }, { path: path.join(projectPath, "src", "App.css"), isDir: false, }, ]; for (const file of filesToRemove) { if (fs.existsSync(file.path)) { try { if (file.isDir) { fs.rmSync(file.path, { recursive: true, force: true }); } else { fs.unlinkSync(file.path); } } catch (error) {} } } } catch (error) {} try { const componentsDir = path.join(projectPath, "src", "components"); if (!fs.existsSync(componentsDir)) { fs.mkdirSync(componentsDir); } const navbarPath = path.join( componentsDir, `Navbar.${language === "JavaScript" ? "jsx" : "tsx"}` ); fs.writeFileSync( navbarPath, `export default function Navbar() {\n return (\n <>\n <h1>Navbar</h1>\n </>\n )\n}` ); const footerPath = path.join( componentsDir, `Footer.${language === "JavaScript" ? "jsx" : "tsx"}` ); fs.writeFileSync( footerPath, `export default function Footer() {\n return (\n <>\n <h1>Footer</h1>\n </>\n )\n}` ); } catch (error) {} // Create routes directory and Routes file if React Router is selected if (packages.includes("React Router")) { try { const routesDir = path.join(projectPath, "src", "routes"); if (!fs.existsSync(routesDir)) { fs.mkdirSync(routesDir); } const routesPath = path.join( routesDir, `Routes.${language === "JavaScript" ? "jsx" : "tsx"}` ); fs.writeFileSync( routesPath, `import { createBrowserRouter } from "react-router-dom"; import Home from "../pages/Home${language === "JavaScript" ? ".jsx" : ".tsx"}"; export const router = createBrowserRouter([ { path: "/", element: <Home />, }, ]);` ); } catch (error) {} } try { const appJsxPath = path.join( projectPath, "src", `App.${language === "JavaScript" ? "jsx" : "tsx"}` ); if (fs.existsSync(appJsxPath)) { try { const appContent = packages.includes("React Router") ? `import { RouterProvider } from "react-router-dom";\nimport { router } from "./routes/Routes${ language === "JavaScript" ? ".jsx" : ".tsx" }";\n\nfunction App() {\n return (\n <>\n <RouterProvider router={router} />\n </>\n );\n}\n\nexport default App;` : `import Navbar from "./components/Navbar";\nimport Home from "./pages/Home";\nimport Footer from "./components/Footer";\n\nexport default function App() {\n return (\n <>\n <Navbar/>\n <Footer/>\n </>\n );\n}`; fs.writeFileSync(appJsxPath, appContent); } catch (error) {} } const indexHtmlPath = path.join(projectPath, "index.html"); if (fs.existsSync(indexHtmlPath)) { try { fs.writeFileSync( indexHtmlPath, `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <link rel=\"stylesheet\" href=\"./src/index.css\" />\n <title>ESYT App</title>\n </head>\n <body>\n <div id=\"root\"></div>\n <script type=\"module\" src=\"/src/main.${ language === "JavaScript" ? "jsx" : "tsx" }\"></script>\n </body>\n</html>\n` ); } catch (error) {} } } catch (error) {} if (installDeps) { try { if (packages.includes("Clerk")) { try { runCommand(`${pm.addCmd} @clerk/clerk-react`, { stdio: "inherit", }); } catch (error) {} } if (packages.includes("Appwrite")) { try { runCommand(`${pm.addCmd} appwrite`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("Prisma")) { try { runCommand(`${pm.addDevCmd} prisma`, { stdio: "inherit" }); runCommand(`${pm.addCmd} @prisma/client`, { stdio: "inherit" }); runCommand( `${pm.dlxCmd} prisma@latest init --datasource-provider postgresql`, { stdio: "inherit" } ); } catch (error) {} } if (packages.includes("React Icons")) { try { runCommand(`${pm.addCmd} react-icons`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("Framer Motion")) { try { runCommand(`${pm.addCmd} framer-motion motion`, { stdio: "inherit", }); } catch (error) {} } if (packages.includes("React Router")) { try { runCommand(`${pm.addCmd} react-router-dom`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("OGL")) { try { runCommand(`${pm.addCmd} ogl`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("Firebase")) { try { runCommand(`${pm.addCmd} firebase`, { stdio: "inherit" }); const firebaseDir = path.join(projectPath, "src", "firebase"); if (!fs.existsSync(firebaseDir)) { fs.mkdirSync(firebaseDir, { recursive: true }); } const firebaseConfigPath = path.join( firebaseDir, "firebase.config.js" ); fs.writeFileSync( firebaseConfigPath, `import { initializeApp } from "firebase/app";\nimport { getAuth } from "firebase/auth";\n\nconst firebaseConfig = {\n apiKey: import.meta.env.VITE_FIREBASE_API_KEY,\n authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,\n projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,\n storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,\n messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,\n appId: import.meta.env.VITE_FIREBASE_APP_ID,\n};\n\nconst app = initializeApp(firebaseConfig);\nexport const auth = getAuth(app);` ); const envPath = path.join(projectPath, ".env"); fs.writeFileSync( envPath, `VITE_FIREBASE_API_KEY=\nVITE_FIREBASE_AUTH_DOMAIN=\nVITE_FIREBASE_PROJECT_ID=\nVITE_FIREBASE_STORAGE_BUCKET=\nVITE_FIREBASE_APP_ID=\nVITE_SERVER_URL=` ); } catch (error) {} } if (packages.includes("DotENV")) { try { runCommand(`${pm.addCmd} dotenv`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("Axios")) { try { runCommand(`${pm.addCmd} axios`, { stdio: "inherit" }); } catch (error) {} } if (packages.includes("TailwindCSS")) { try { runCommand(`${pm.addCmd} tailwindcss @tailwindcss/vite`, { stdio: "inherit", }); const indexCssPath = path.join(projectPath, "src", "index.css"); if (fs.existsSync(indexCssPath)) { fs.writeFileSync(indexCssPath, `@import "tailwindcss";\n`); } const tailwindConfigPath = path.join( projectPath, "tailwind.config.js" ); try { if (!fs.existsSync(tailwindConfigPath)) { fs.writeFileSync( tailwindConfigPath, `/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\n "./index.html",\n "./src/**/*.{js,ts,jsx,tsx}",\n ],\n theme: {\n extend: {},\n },\n plugins: [],\n};\n` ); } } catch (error) {} const viteConfigExt = language === "TypeScript" ? "ts" : "js"; const viteConfigPath = path.join( projectPath, `vite.config.${viteConfigExt}` ); if (fs.existsSync(viteConfigPath)) { fs.writeFileSync( viteConfigPath, `import { defineConfig } from "vite";\nimport react from "@vitejs/plugin-react";\nimport tailwindcss from "@tailwindcss/vite";\n\nexport default defineConfig({\n plugins: [react(), tailwindcss()],\n});\n` ); } } catch (error) {} } } catch (error) {} } // Install any arbitrary npm packages provided via flags if (flags.npmPackages && flags.npmPackages.length > 0 && installDeps) { for (const np of flags.npmPackages) { try { console.log(`Installing extra package: ${np}`); runCommand(`${pm.addCmd} ${np}`, { stdio: "inherit" }); } catch (error) { console.error(`Failed to install ${np}: ${error.message}`); } } } try { const pagesDir = path.join(projectPath, "src", "pages"); if (!fs.existsSync(pagesDir)) { fs.mkdirSync(pagesDir); } const homePath = path.join( pagesDir, `Home.${language === "JavaScript" ? "jsx" : "tsx"}` ); fs.writeFileSync( homePath, `export default function Home() {\n return (\n <>\n <h1>Home</h1>\n </>\n )\n}` ); } catch (error) {} } else if (framework === "Next.js") { // --- Next.js extra options (handled automatically) --- // These options are now automated with sensible defaults: // - ESLint: enabled // - src/ directory: disabled // - App Router: enabled // - Turbopack: enabled const nextOptions = { eslint: true, srcDir: false, appRouter: true, turbo: true, }; // Tailwind CSS autofill const useTailwind = packages.includes("TailwindCSS"); // --- Next.js project creation --- const nextFlagsParts = []; // Set language flag for Next.js if (language === "TypeScript") { nextFlagsParts.push("--ts"); } else { nextFlagsParts.push("--js"); } nextFlagsParts.push(nextOptions.eslint ? "--eslint" : "--no-eslint"); nextFlagsParts.push(useTailwind ? "--tailwind" : "--no-tailwind"); nextFlagsParts.push(nextOptions.srcDir ? "--src-dir" : "--no-src-dir"); nextFlagsParts.push(nextOptions.appRouter ? "--app" : "--no-app"); // Add Turbopack flags only if selected, per official docs (Made the import alias static to yes as it was not possible to automate for no) if (nextOptions.turbo) { nextFlagsParts.push("--turbopack", '--import-alias "@/*"'); } // Add React Compiler flag (disabled) to suppress prompt nextFlagsParts.push("--no-react-compiler"); const nextCommand = pm.createNextCmd( projectName, nextFlagsParts.join(" ") ); console.log(`Running: ${nextCommand}`); runCommand(nextCommand, { stdio: "inherit" }); console.log("Next.js project created."); process.chdir(projectPath); // Cleanup: Remove README.md for Next.js projects (to match Vite cleanup) try { const readmePath = path.join(projectPath, "README.md"); if (fs.existsSync(readmePath)) { fs.unlinkSync(readmePath); } } catch (error) {} console.log(`Changed directory to: ${projectName}`); try { if (installDeps) { console.log("Installing initial dependencies..."); runCommand(pm.installCmd, { stdio: "inherit" }); } else { console.log( "Skipping initial dependencies installation as requested." ); } } catch (error) { console.error("Error during initial setup:", error.message); } // Scaffold components directory and Navbar/Footer try { // Determine base app directory for components let baseAppDir = projectPath; if (nextOptions.srcDir) { baseAppDir = path.join(projectPath, "src", "app"); } else { baseAppDir = path.join(projectPath, "app"); } if (!fs.existsSync(baseAppDir)) { fs.mkdirSync(baseAppDir, { recursive: true }); } const componentsDir = path.join(baseAppDir, "components"); if (!fs.existsSync(componentsDir)) { fs.mkdirSync(componentsDir); console.log("Created components directory inside app."); } const ext = language === "JavaScript" ? "jsx" : "tsx"; const navbarPath = path.join(componentsDir, `Navbar.${ext}`); fs.writeFileSync( navbarPath, `export default function Navbar() {\n return (\n <nav className="p-16 border-b border-slate-200">\n <h1>Navbar</h1>\n </nav>\n )\n}` ); const footerPath = path.join(componentsDir, `Footer.${ext}`); fs.writeFileSync( footerPath, `export default function Footer() {\n return (\n <footer className="p-16 border-t border-slate-200">\n <h1>Footer</h1>\n </footer>\n )\n}` ); console.log( "Created Navbar and Footer components inside app/components." ); } catch (error) { console.error("Error creating components:", error.message); } // Scaffold sample API route try { const apiDir = path.join(projectPath, "pages", "api"); if (!fs.existsSync(apiDir)) { fs.mkdirSync(apiDir, { recursive: true }); } const apiFile = path.join( apiDir, `hello.${language === "JavaScript" ? "js" : "ts"}` ); fs.writeFileSync( apiFile, language === "JavaScript" ? `export default function handler(req, res) {\n res.status(200).json({ message: 'Hello from Next.js API!' });\n}` : `import { NextApiRequest, NextApiResponse } from 'next';\nexport default function handler(req: NextApiRequest, res: NextApiResponse) {\n res.status(200).json({ message: 'Hello from Next.js API!' });\n}` ); console.log("Created sample API route."); } catch (error) { console.error("Error creating API route:", error.message); } // Scaffold layout file in app directory try { let baseAppDir = projectPath; if (nextOptions.srcDir) { baseAppDir = path.join(projectPath, "src", "app"); } else { baseAppDir = path.join(projectPath, "app"); } const ext = language === "TypeScript" ? "tsx" : language === "JavaScript" ? "jsx" : "js"; const layoutFile = path.join(baseAppDir, `layout.${ext}`); const layoutCode = `import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"], }); const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], }); export const metadata: Metadata = { title: "ESYT App", description: "Project created with ESYT", }; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( <html lang=\"en\"> <body className={\`${geistSans.variable} ${geistMono.variable} antialiased\`} > {children} </body> </html> ); } `; fs.writeFileSync(layoutFile, layoutCode); console.log(`Created layout file as app/layout.${ext}`); } catch (error) { console.error("Error creating layout file:", error.message); } // Scaffold .env.local try { const envPath = path.join(projectPath, ".env.local"); if (!fs.existsSync(envPath)) { fs.writeFileSync(envPath, `NEXT_PUBLIC_EXAMPLE=\n`); console.log("Created .env.local file."); } } catch (error) { console.error("Error creating .env.local:", error.message); } // Overwrite main page file in app directory for Next.js try { let baseAppDir = projectPath; if (nextOptions.srcDir) { baseAppDir = path.join(projectPath, "src", "app"); } else { baseAppDir = path.join(projectPath, "app"); } const ext = language === "TypeScript" ? "tsx" : "jsx"; const mainPageFile = path.join(baseAppDir, `page.${ext}`); fs.writeFileSync( mainPageFile, `export default function Home() {\n return (\n <div>\n <h1>ESYT</h1>\n </div>\n );\n}` ); console.log(`Overwritten main page as app/page.${ext}`); } catch (error) { console.error("Error writing main page:", error.message); } // Install selected packages (Next.js compatible) if (installDeps && packages.length > 0) { console.log("\nInstalling selected packages..."); for (const pkg of packages) { try { switch (pkg) { case "TailwindCSS": // For Next.js, install tailwindcss, @tailwindcss/postcss, postcss execSync( `${pm.addCmd} tailwindcss @tailwindcss/postcss postcss`, { stdio: "inherit" } ); // Write postcss.config.mjs const postcssConfigPath = path.join( projectPath, "postcss.config.mjs" ); fs.writeFileSync( postcssConfigPath, 'const config = { plugins: { "@tailwindcss/postcss": {}, },};\nexport default config;' ); // Update globals.css in app or src/app let baseAppDir = projectPath; if (nextOptions.srcDir) { baseAppDir = path.join(projectPath, "src", "app"); } else { baseAppDir = path.join(projectPath, "app"); } const globalsCssPath = path.join(baseAppDir, "globals.css"); fs.writeFileSync(globalsCssPath, '@import "tailwindcss";\n'); break; case "React Icons": runCommand(`${pm.addCmd} react-icons`, { stdio: "inherit" }); break; case "Framer Motion": runCommand(`${pm.addCmd} framer-motion`, { stdio: "inherit" }); break; case "DotENV": runCommand(`${pm.addCmd} dotenv`, { stdio: "inherit" }); break; case "Axios": runCommand(`${pm.addCmd} axios`, { stdio: "inherit" }); break; case "Firebase": runCommand(`${pm.addCmd} firebase`, { stdio: "inherit" }); break; case "Clerk": runCommand(`${pm.addCmd} @clerk/nextjs`, { stdio: "inherit" }); break; case "Appwrite": runCommand(`${pm.addCmd} appwrite`, { stdio: "inherit" }); break; case "Prisma": runCommand(`${pm.addDevCmd} prisma`, { stdio: "inherit" }); runCommand(`${pm.addCmd} @prisma/client`, { stdio: "inherit" }); runCommand( `${pm.dlxCmd} prisma@latest init --datasource-provider postgresql`, { stdio: "inherit" } ); break; case "next-auth": runCommand(`${pm.addCmd} next-auth`, { stdio: "inherit" }); break; case "@next/font": runCommand(`${pm.addCmd} @next/font`, { stdio: "inherit" }); break; case "next-seo": runCommand(`${pm.addCmd} next-seo`, { stdio: "inherit" }); break; case "next-sitemap": runCommand(`${pm.addCmd} next-sitemap`, { stdio: "inherit" }); break; case "next-pwa": runCommand(`${pm.addCmd} next-pwa`, { stdio: "inherit" }); break; } console.log(`✅ ${pkg} installed.`); } catch (error) { console.error(`❌ Failed to install ${pkg}: ${error.message}`); } } // Install any arbitrary npm packages provided via flags if (flags.npmPackages && flags.npmPackages.length > 0) { for (const np of flags.npmPackages) { try { console.log(`Installing extra package: ${np}`); runCommand(`${pm.addCmd} ${np}`, { stdio: "inherit" }); } catch (error) { console.error(`Failed to install ${np}: ${error.message}`); } } } } } // --- End framework-specific logic --- // --- Git init, IDE, dev server, etc. (shared) --- if (gitInit) { try { console.log("\nInitializing Git repository..."); runCommand("git init", { stdio: "inherit" }); const gitignorePath = path.join(projectPath, ".gitignore"); if (!fs.existsSync(gitignorePath)) { fs.writeFileSync( gitignorePath, `# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\n# Dependencies\nnode_modules\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n# Environment variables\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n` ); console.log("Created .gitignore file."); } console.log("✅ Git repository initialized."); } catch (error) { console.error( `❌ Failed to initialize Git repository: ${error.message}` ); } } else { // Remove .git folder if it exists const gitFolder = path.join(projectPath, ".git"); if (fs.existsSync(gitFolder)) { fs.rmSync(gitFolder, { recursive: true, force: true }); } } // Open the project with the selected IDE try { if (selectedIDE !== "None") { console.log(`\nOpening project with ${selectedIDE}...`); try { let ideCommand = ""; switch (selectedIDE) { case "Nvim": ideCommand = "nvim ."; break; case "Zed": ideCommand = "zed ."; break; case "VSCode": ideCommand = "code ."; break; case "Cursor": ideCommand = "cursor ."; break; case "Trae": ideCommand = "trae ."; break; } if (ideCommand) { runCommand(ideCommand, { stdio: "inherit" }); console.log(`✅ Project opened with ${selectedIDE}.`); } } catch (error) { console.error( `❌ Failed to open project with ${selectedIDE}: ${error.message}` ); console.log( `To open manually, run: 'cd ${projectName}' and then the appropriate IDE command.` ); } } } catch (error) { console.error("Error during IDE launch:", error.message); } // Run dev server if selected if (runDevServer) { try { console.log(`\nStarting development server with ${pm.name}...`); // Change to project directory process.chdir(projectPath); // Run the dev server command runCommand(`${pm.name} run dev`, { stdio: "inherit" }); } catch (error) { console.error( `❌ Failed to start development server: ${error.message}` ); console.log( `You can manually start it by running '${pm.name} run dev' in your project directory.` ); } } console.log("\n✨ Project setup complete! ✨"); } catch (error) { console.error( "\n❌ An error occurred during project setup:", error.message ); console.error("Please try again or report this issue."); process.exit(1); } } run().catch((error) => { console.error("\n❌ An unexpected error occurred:", error.message); console.error("Please try again or report this issue."); process.exit(1); });