UNPKG

create-filip-app

Version:

A modern CLI tool for creating Next.js applications with best practices, shadcn components, and MongoDB integration.

1,364 lines (1,189 loc) 126 kB
#!/usr/bin/env node import prompts from "prompts"; import { execa } from "execa"; import fs from "fs-extra"; import path from "path"; import ora from "ora"; import chalk from "chalk"; import cliProgress from "cli-progress"; import gradient from "gradient-string"; import boxen from "boxen"; import inquirer from "inquirer"; import inquirerSearchCheckbox from 'inquirer-search-checkbox'; import { spawn } from 'child_process'; import os from 'os'; import * as templates from './templates.js'; // Get package version from package.json const packageJson = JSON.parse(await fs.readFile(new URL('./package.json', import.meta.url), 'utf8')); const VERSION = packageJson.version || '0.0.0'; // Register checkbox search prompt inquirer.registerPrompt('search-checkbox', inquirerSearchCheckbox); // Theme configuration const themes = { default: { primary: ['#FF5F6D', '#FFC371'], secondary: 'cyan', accent: 'green', error: 'red' }, ocean: { primary: ['#2E3192', '#1BFFFF'], secondary: 'blue', accent: 'cyan', error: 'red' }, forest: { primary: ['#134E5E', '#71B280'], secondary: 'green', accent: 'yellow', error: 'red' } }; let currentTheme = themes.default; const filip = gradient(currentTheme.primary); function applyTheme(themeName) { currentTheme = themes[themeName] || themes.default; return gradient(currentTheme.primary); } function printBanner() { const gradientFn = gradient(currentTheme.primary); console.log(gradientFn(` ███████╗██╗██╗ ██╗██████╗ ██╔════╝██║██║ ██║██╔══██╗ █████╗ ██║██║ ██║██████╔╝ ██╔══╝ ██║██║ ██║██╔═══╝ ██║ ██║███████╗██║██║ ╚═╝ ╚═╝╚══════╝╚═╝╚═╝ `)); console.log(`${gradientFn('🚀')} ${chalk.bold("Create Filip App")} ${chalk.dim(`v${VERSION}`)} ${gradientFn('✨')}\n`); console.log(chalk.dim("Modern Next.js setup with all the best practices\n")); } async function checkPackageManager() { try { await execa("pnpm", ["--version"]); return "pnpm"; } catch (e) { try { await execa("yarn", ["--version"]); return "yarn"; } catch (e) { return "npm"; } } } async function showWelcomeScreen() { const gradientFn = gradient(currentTheme.primary); console.log(boxen( `${chalk.bold(gradientFn('Welcome to the Filip App Creator!'))}\n\n` + `${chalk.dim('This tool helps you create a production-ready Next.js app with:')}\n\n` + ` ${chalk.green('•')} Next.js 14 with App Router\n` + ` ${chalk.green('•')} TypeScript for type safety\n` + ` ${chalk.green('•')} TailwindCSS for styling\n` + ` ${chalk.green('•')} Components system (UI components)\n` + ` ${chalk.green('•')} Dark mode support\n` + ` ${chalk.green('•')} ESLint configuration\n`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan', title: '✨ Welcome', titleAlignment: 'center' } )); const { action } = await inquirer.prompt([ { type: 'list', name: 'action', message: 'What would you like to do?', prefix: gradientFn('?'), choices: [ { name: `${chalk.bold('🔨 Create')} ${chalk.dim('a new Filip app')}`, value: 'create' }, { name: `${chalk.bold('🎨 Theme')} ${chalk.dim('change the look')}`, value: 'theme' }, { name: `${chalk.bold('📝 Version')} ${chalk.dim('check or update version')}`, value: 'version' }, { name: `${chalk.bold('ℹ️ About')} ${chalk.dim('this tool')}`, value: 'about' }, { name: `${chalk.bold('❌ Exit')}`, value: 'exit' } ] } ]); if (action === 'exit') { console.log(chalk.yellow("\nExiting. Goodbye! 👋")); process.exit(0); } else if (action === 'version') { const gradientFn = gradient(currentTheme.primary); console.log(boxen( `${chalk.bold(gradientFn('Current Version'))}\n\n` + `${chalk.bold(VERSION)}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'magenta', title: '🔢 Version', titleAlignment: 'center' } )); const { versionAction } = await inquirer.prompt([ { type: 'list', name: 'versionAction', message: 'What would you like to do?', choices: [ { name: 'Update version (semantic)', value: 'update' }, { name: 'Back to main menu', value: 'back' } ] } ]); if (versionAction === 'update') { // Parse current version const [major, minor, patch] = VERSION.split('.').map(Number); const { updateType } = await inquirer.prompt([ { type: 'list', name: 'updateType', message: 'Select version update type:', choices: [ { name: `Major (${major + 1}.0.0)`, value: 'major' }, { name: `Minor (${major}.${minor + 1}.0)`, value: 'minor' }, { name: `Patch (${major}.${minor}.${patch + 1})`, value: 'patch' }, { name: 'Custom version', value: 'custom' } ] } ]); let newVersion; if (updateType === 'major') { newVersion = `${major + 1}.0.0`; } else if (updateType === 'minor') { newVersion = `${major}.${minor + 1}.0`; } else if (updateType === 'patch') { newVersion = `${major}.${minor}.${patch + 1}`; } else { const { customVersion } = await inquirer.prompt([ { type: 'input', name: 'customVersion', message: 'Enter new version (x.y.z format):', default: VERSION, validate: (input) => { return /^\d+\.\d+\.\d+$/.test(input) ? true : 'Please enter a valid version in x.y.z format'; } } ]); newVersion = customVersion; } // Update the version const spinner = ora({ text: `Updating version to ${newVersion}...`, color: 'magenta' }).start(); const success = await updatePackageVersion(newVersion); if (success) { spinner.succeed(`Version updated to ${newVersion}`); console.log(chalk.yellow('\nPlease restart the CLI to apply the new version.\n')); } else { spinner.fail('Failed to update version'); } } return showWelcomeScreen(); } else if (action === 'about') { const gradientFn = gradient(currentTheme.primary); console.log(boxen( `${chalk.bold(gradientFn('Create Filip App'))}\n\n` + `A modern CLI tool for creating Next.js applications with best practices.\n\n` + `${chalk.bold('Author:')} Filip\n` + `${chalk.bold('Version:')} ${VERSION}\n` + `${chalk.bold('License:')} MIT`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', title: 'About', titleAlignment: 'center' } )); return showWelcomeScreen(); } else if (action === 'theme') { const { selectedTheme } = await inquirer.prompt([ { type: 'list', name: 'selectedTheme', message: 'Select a theme:', choices: [ { name: '🔥 Default (Warm)', value: 'default' }, { name: '🌊 Ocean Blue', value: 'ocean' }, { name: '🌲 Forest Green', value: 'forest' } ] } ]); applyTheme(selectedTheme); console.log(chalk.green(`\nTheme changed to ${selectedTheme}!\n`)); return showWelcomeScreen(); } return action; } // Function to update the package.json version async function updatePackageVersion(newVersion) { try { // Read the current package.json const packageJsonPath = new URL('./package.json', import.meta.url); const packageData = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); // Update the version packageData.version = newVersion; // Write back to package.json with proper formatting await fs.writeFile( packageJsonPath, JSON.stringify(packageData, null, 2) + '\n', 'utf8' ); console.log(chalk.green(`✓ Package version updated to ${newVersion}`)); return true; } catch (error) { console.error(chalk.red(`Failed to update package version: ${error.message}`)); return false; } } // Helper function to set up project manually async function setupProjectManually(projectName, projectDir, pkgManager) { console.log(chalk.cyan(`Setting up project manually...`)); try { // Create the base directory await fs.ensureDir(projectDir); // Create package.json const basePackageJson = { name: projectName, version: "0.1.0", private: true, scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" }, dependencies: { next: "latest", react: "latest", "react-dom": "latest" }, devDependencies: { typescript: "latest", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", "@tailwindcss/postcss": "latest", "tailwindcss": "latest", "postcss": "latest", "autoprefixer": "latest", "eslint": "latest", "eslint-config-next": "latest" } }; await fs.writeJson(path.join(projectDir, 'package.json'), basePackageJson, { spaces: 2 }); // Create basic Next.js structure await fs.ensureDir(path.join(projectDir, 'app')); await fs.ensureDir(path.join(projectDir, 'public')); // Create a basic page.tsx // Choose page template based on whether components are being installed let pageTemplate; if (features && features.installComponents) { pageTemplate = templates.componentsShowcasePage; } else { pageTemplate = templates.pageContent(projectName); } await fs.writeFile(path.join(projectDir, 'app', 'page.tsx'), pageTemplate); // Create a basic layout.tsx const layoutContent = templates.layoutContent; await fs.writeFile(path.join(projectDir, 'app', 'layout.tsx'), layoutContent); // Create minimal config files const nextConfigContent = templates.nextConfigContent; await fs.writeFile(path.join(projectDir, 'next.config.js'), nextConfigContent); const tsConfigContent = templates.tsConfigContent; await fs.writeFile(path.join(projectDir, 'tsconfig.json'), tsConfigContent); // Create PostCSS config const postcssConfigContent = templates.postcssConfigContent; await fs.writeFile(path.join(projectDir, 'postcss.config.mjs'), postcssConfigContent); // Create the CSS file const globalCssContent = templates.globalCssContent; await fs.ensureDir(path.join(projectDir, 'app')); await fs.writeFile(path.join(projectDir, 'app', 'globals.css'), globalCssContent); console.log(chalk.green(`Created basic Next.js app structure manually`)); console.log(boxen( `${chalk.bold(gradient(currentTheme.primary)('Manual Setup Complete'))}\n\n` + `${chalk.cyan('Basic Next.js app structure created. You may need to run:')} \n\n` + `${chalk.dim(`${pkgManager} install`)} \n\n` + `${chalk.cyan('to complete the setup after installation.')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', title: '📦 Manual Setup', titleAlignment: 'center' } )); return true; } catch (manualError) { console.error(chalk.red(`Failed to set up project manually: ${manualError.message}`)); return false; } } async function run() { // Define gradient function at the beginning to prevent reference errors const runGradient = text => gradient(currentTheme.primary)(text); printBanner(); const action = await showWelcomeScreen(); if (action !== 'create') return; // Project configuration const config = await inquirer.prompt([ { type: 'input', name: 'appName', message: 'What is your project name?', default: 'filip-app', validate: input => { if (input.trim() === '') return 'Project name cannot be empty'; // Check if directory already exists const dirPath = path.resolve(process.cwd(), input.trim()); if (fs.existsSync(dirPath)) { const dirContents = fs.readdirSync(dirPath); if (dirContents.length > 0) { return `Directory "${input}" already exists and is not empty. Please choose a different name or use an empty directory.`; } } return true; } }, { type: 'list', name: 'packageManager', message: 'Select preferred package manager:', choices: [ { name: '🔍 Auto-detect (recommended)', value: 'auto' }, { name: '📦 npm', value: 'npm' }, { name: '🚀 pnpm', value: 'pnpm' }, { name: '🧶 yarn', value: 'yarn' } ], default: 'auto' }, { type: 'checkbox', name: 'features', message: 'Select additional features:', choices: [ { name: '✅ Install UI components', value: 'components', checked: true }, { name: '⚡ Ultra-fast mode (10x faster setup)', value: 'fastMode', checked: true }, { name: '🌓 Light/Dark theme switcher', value: 'themeSwitch', checked: true }, { name: '💾 MongoDB + Prisma setup', value: 'prisma', checked: false }, { name: '🔐 Authentication (Next-Auth)', value: 'auth', checked: false }, { name: '📄 Add documentation files', value: 'docs', checked: false }, { name: '📱 Add responsive layouts', value: 'responsive', checked: false } ] } ]); // Extract config const { appName, packageManager: pmChoice } = config; const features = { installComponents: config.features.includes('components'), fastMode: config.features.includes('fastMode'), themeSwitch: config.features.includes('themeSwitch'), prisma: config.features.includes('prisma'), auth: config.features.includes('auth'), includeDocs: config.features.includes('docs'), responsive: config.features.includes('responsive') }; const appDir = path.resolve(process.cwd(), appName); // Determine package manager let packageManager = pmChoice; if (packageManager === 'auto') { packageManager = await checkPackageManager(); } // Show component selection if components are enabled let selectedComponents = ['button', 'card', 'sheet', 'dropdown-menu', 'tabs', 'input', 'label']; if (features.installComponents) { // Create arrays of available components - separated by type const shadcnComponents = [ 'accordion', 'alert', 'alert-dialog', 'aspect-ratio', 'avatar', 'badge', 'breadcrumb', 'button', 'calendar', 'card', 'carousel', 'chart', 'checkbox', 'collapsible', 'combobox', 'command', 'context-menu', 'data-table', 'date-picker', 'dialog', 'drawer', 'dropdown-menu', 'form', 'hover-card', 'input', 'input-otp', 'label', 'menubar', 'navigation-menu', 'pagination', 'popover', 'progress', 'radio-group', 'resizable', 'scroll-area', 'select', 'separator', 'sheet', 'sidebar', 'skeleton', 'slider', 'sonner', 'switch', 'table', 'tabs', 'textarea', 'toast', 'toggle', 'toggle-group', 'tooltip' ]; const customComponents = [ 'file-uploader', 'video-player', 'timeline', 'rating', 'file-tree', 'copy-button' ]; // Combine all components const allComponents = [...shadcnComponents, ...customComponents]; // Define essential components with additional important ones const essentialComponents = [ 'button', 'card', 'dialog', 'dropdown-menu', 'form', 'input', 'label', 'sheet', 'tabs', 'separator', 'toast', 'accordion', 'slider' ]; // First, ask if the user wants to select components const { componentSelection } = await inquirer.prompt([ { type: 'list', name: 'componentSelection', message: 'How would you like to select Components?', choices: [ { name: '🔍 Custom selection (choose specific components)', value: 'custom' }, { name: '✅ Select all components', value: 'all' }, { name: '🧰 Essential components only (common UI elements)', value: 'essential' } ] } ]); if (componentSelection === 'all') { selectedComponents = allComponents; console.log(chalk.green(`\n✓ Selected all ${selectedComponents.length} components\n`)); } else if (componentSelection === 'essential') { selectedComponents = essentialComponents; console.log(chalk.green(`\n✓ Selected essential components: ${selectedComponents.join(', ')}\n`)); } else { // Show the component selection dialog for custom selection with grouping const componentPrompt = await inquirer.prompt([ { type: 'checkbox', // Change from search-checkbox to regular checkbox name: 'components', message: 'Select components to install:', choices: [ new inquirer.Separator('--- UI Components ---'), { name: 'Accordion', value: 'accordion' }, { name: 'Alert', value: 'alert' }, { name: 'Alert Dialog', value: 'alert-dialog' }, { name: 'Aspect Ratio', value: 'aspect-ratio' }, { name: 'Avatar', value: 'avatar' }, { name: 'Badge', value: 'badge' }, { name: 'Breadcrumb', value: 'breadcrumb' }, { name: 'Button', value: 'button', checked: true }, { name: 'Calendar', value: 'calendar' }, { name: 'Card', value: 'card', checked: true }, { name: 'Carousel', value: 'carousel' }, { name: 'Chart', value: 'chart' }, { name: 'Checkbox', value: 'checkbox' }, { name: 'Collapsible', value: 'collapsible' }, { name: 'Combobox', value: 'combobox' }, { name: 'Command', value: 'command' }, { name: 'Context Menu', value: 'context-menu' }, { name: 'Data Table', value: 'data-table' }, { name: 'Date Picker', value: 'date-picker' }, { name: 'Dialog', value: 'dialog', checked: true }, { name: 'Drawer', value: 'drawer' }, { name: 'Dropdown Menu', value: 'dropdown-menu', checked: true }, { name: 'Form', value: 'form', checked: true }, { name: 'Hover Card', value: 'hover-card' }, { name: 'Input', value: 'input', checked: true }, { name: 'Input OTP', value: 'input-otp' }, { name: 'Label', value: 'label', checked: true }, { name: 'Menubar', value: 'menubar' }, { name: 'Navigation Menu', value: 'navigation-menu' }, { name: 'Pagination', value: 'pagination' }, { name: 'Popover', value: 'popover' }, { name: 'Progress', value: 'progress' }, { name: 'Radio Group', value: 'radio-group' }, { name: 'Resizable', value: 'resizable' }, { name: 'Scroll Area', value: 'scroll-area' }, { name: 'Select', value: 'select' }, { name: 'Separator', value: 'separator' }, { name: 'Sheet', value: 'sheet' }, { name: 'Sidebar', value: 'sidebar' }, { name: 'Skeleton', value: 'skeleton' }, { name: 'Slider', value: 'slider' }, { name: 'Sonner', value: 'sonner' }, { name: 'Switch', value: 'switch' }, { name: 'Table', value: 'table' }, { name: 'Tabs', value: 'tabs' }, { name: 'Textarea', value: 'textarea' }, { name: 'Toast', value: 'toast' }, { name: 'Toggle', value: 'toggle' }, { name: 'Toggle Group', value: 'toggle-group' }, { name: 'Tooltip', value: 'tooltip' }, new inquirer.Separator('--- Custom Components ---'), { name: 'File Uploader (Drag & Drop)', value: 'file-uploader', checked: true }, { name: 'Video Player Interface', value: 'video-player' }, { name: 'Timeline', value: 'timeline' }, { name: 'Rating/Stars Input', value: 'rating' }, { name: 'File Tree', value: 'file-tree' }, { name: 'Copy to Clipboard Button', value: 'copy-button', checked: true } ], pageSize: 20 } ]); if (componentPrompt.components.length > 0) { selectedComponents = componentPrompt.components; } } } // Show configuration summary console.log(boxen( `${chalk.bold(runGradient('✨ Project Configuration ✨'))}\n\n` + `${chalk.bold('Project:')} ${chalk.cyan(appName)}\n` + `${chalk.bold('Location:')} ${chalk.cyan(appDir)}\n` + `${chalk.bold('Package Manager:')} ${chalk.cyan(packageManager)}\n\n` + `${chalk.bold('Features:')}\n` + ` ${features.installComponents ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('UI Components:')} ${features.installComponents ? chalk.cyan(selectedComponents.length > 5 ? selectedComponents.slice(0, 5).join(', ') + ', ...' : selectedComponents.join(', ')) : chalk.gray('None')}\n` + ` ${features.fastMode ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Fast Mode')}\n` + ` ${features.themeSwitch ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Theme Switcher')}\n` + ` ${features.prisma ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Prisma Database')}\n` + ` ${features.auth ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Authentication')}\n` + ` ${features.includeDocs ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Documentation')}\n` + ` ${features.responsive ? chalk.green('✓') : chalk.gray('○')} ${chalk.bold('Responsive Layout')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: currentTheme.secondary, title: '🛠️ Configuration', titleAlignment: 'center' } )); // Confirm and continue const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Create the application with these settings?', default: true } ]); if (!confirm) { console.log(chalk.yellow("\nCancelled. Let's start over!")); return run(); } const spinner = ora({ color: currentTheme.secondary, spinner: 'dots' }); const steps = [ "Creating Next.js app", "Installing dependencies", "Setting up shadcn components", "Creating .env file" ]; if (features.themeSwitch) { steps.push("Setting up theme switcher"); } if (features.prisma) { steps.push("Initializing Prisma"); } if (features.auth) { steps.push("Setting up authentication"); } if (features.installComponents) { steps.push("Installing UI components"); } if (features.includeDocs) { steps.push("Adding documentation"); } if (features.responsive) { steps.push("Setting up responsive layouts"); } const progressBar = new cliProgress.SingleBar({ format: runGradient("Installation") + " " + chalk.dim("|") + chalk[currentTheme.secondary]("{bar}") + chalk.dim("|") + " " + chalk.bold("{percentage}%") + chalk.dim(" •") + " " + chalk.white("{step}"), barCompleteChar: "█", barIncompleteChar: "░", hideCursor: true, clearOnComplete: true, barsize: 30 }); // Create a function to update progress with better visuals const updateProgress = (step, incrementBy = 1) => { currentStep += incrementBy; progressBar.update(currentStep, { step: chalk.cyan(step) }); spinner.text = step; }; // Initialize progress tracking let currentStep = 0; progressBar.start(steps.length, currentStep, { step: "Starting..." }); try { // Step 1: Create Next.js app updateProgress(`${steps[0]}...`); spinner.stop(); // Show a creating app box rather than plain text console.log(boxen( `${chalk.bold(runGradient('Creating Next.js App'))}\n\n` + `${chalk.cyan('•')} Project name: ${chalk.bold(appName)}\n` + `${chalk.cyan('•')} Directory: ${chalk.dim(appDir)}\n` + `${chalk.cyan('•')} Package manager: ${chalk.bold(packageManager)}\n` + `${chalk.cyan('•')} Features: ${chalk.dim('TypeScript, ESLint, Tailwind CSS')}\n`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: currentTheme.secondary, title: '🚀 New Project', titleAlignment: 'center' } )); // Create a loader for the app creation process const createAppSpinner = ora({ text: chalk.cyan('Initializing Next.js app (this might take a moment)...'), color: 'cyan', spinner: 'dots' }).start(); // Check if directory exists and is not empty before running create-next-app const appDirExists = fs.existsSync(appDir); if (appDirExists) { const dirContents = fs.readdirSync(appDir); if (dirContents.length > 0) { const { handleExisting } = await inquirer.prompt([ { type: 'list', name: 'handleExisting', message: `Directory "${appName}" already exists and contains files. What would you like to do?`, choices: [ { name: 'Choose a different name', value: 'rename' }, { name: 'Delete existing directory and start fresh', value: 'delete' }, { name: 'Cancel installation', value: 'cancel' } ] } ]); if (handleExisting === 'rename') { const { newName } = await inquirer.prompt([ { type: 'input', name: 'newName', message: 'Enter a new project name:', validate: input => { if (input.trim() === '') return 'Project name cannot be empty'; const newDir = path.resolve(process.cwd(), input.trim()); if (fs.existsSync(newDir)) { const newDirContents = fs.readdirSync(newDir); if (newDirContents.length > 0) { return `Directory "${input}" also exists and is not empty. Please choose a different name.`; } } return true; } } ]); // Update app name and directory appName = newName.trim(); appDir = path.resolve(process.cwd(), appName); } else if (handleExisting === 'delete') { const { confirmDelete } = await inquirer.prompt([ { type: 'confirm', name: 'confirmDelete', message: `Are you sure you want to delete the existing "${appName}" directory? This cannot be undone.`, default: false } ]); if (confirmDelete) { try { spinner.start(`Deleting existing directory ${appName}...`); await fs.remove(appDir); spinner.succeed(`Deleted existing directory ${appName}`); } catch (deleteError) { spinner.fail(`Failed to delete directory: ${deleteError.message}`); throw new Error(`Cannot proceed: failed to delete existing directory ${appName}`); } } else { throw new Error('Installation cancelled: directory already exists'); } } else { throw new Error('Installation cancelled by user'); } } } // Determine package manager flags for create-next-app const pmFlag = packageManager === "npm" ? "--use-npm" : packageManager === "pnpm" ? "--use-pnpm" : packageManager === "yarn" ? "--use-yarn" : "--use-npm"; // After the create-next-app command is executed, capture the output but display it in a better format try { // Create a function to verify app creation success const verifyAppCreation = async () => { // Check if the directory was actually created if (!fs.existsSync(appDir)) { return false; } // Check if the directory contains basic Next.js files try { const dirContents = await fs.readdir(appDir); return dirContents.some(file => ['package.json', 'next.config.js', 'app', 'tsconfig.json'].includes(file) ); } catch (err) { return false; } }; // Execute the create-next-app command but capture output instead of showing raw createAppSpinner.text = chalk.cyan(`Running create-next-app for ${chalk.bold(appName)}...`); try { const { stdout } = await execa("npx", [ "create-next-app@latest", appName, "--ts", "--tailwind", "--eslint", "--app", "--src-dir", "--import-alias", "@/*", pmFlag ], { stdio: ["ignore", "pipe", "pipe"], timeout: 180000 // 3 minutes timeout }); // Verify the app was created successfully const isCreated = await verifyAppCreation(); if (!isCreated) { throw new Error("Next.js app directory wasn't created properly despite command succeeding"); } createAppSpinner.succeed(chalk.green(`Next.js app created successfully`)); // Parse the output to extract useful information const dependencies = stdout.match(/dependencies:\n([\s\S]*?)(?:\n\n|$)/); const devDependencies = stdout.match(/devDependencies:\n([\s\S]*?)(?:\n\n|$)/); // Display the dependencies in a nicely formatted box if (dependencies || devDependencies) { console.log(boxen( `${chalk.bold(runGradient('Packages Installed'))}\n\n` + `${dependencies ? `${chalk.cyan('Dependencies:')}\n${chalk.dim(dependencies[1])}\n` : ''}` + `${devDependencies ? `${chalk.cyan('Dev Dependencies:')}\n${chalk.dim(devDependencies[1])}` : ''}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '📦 Dependencies', titleAlignment: 'center' } )); } } catch (cmdError) { // If the command failed, try a different approach with fs-extra createAppSpinner.text = chalk.cyan(`First approach failed. Trying alternative method for ${chalk.bold(appName)}...`); // Create the base directory await fs.ensureDir(appDir); // Create package.json const basePackageJson = { name: appName, version: "0.1.0", private: true, scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" }, dependencies: { next: "latest", react: "latest", "react-dom": "latest" }, devDependencies: { typescript: "latest", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", "@tailwindcss/postcss": "latest", "tailwindcss": "latest", "postcss": "latest", "autoprefixer": "latest", "eslint": "latest", "eslint-config-next": "latest" } }; await fs.writeJson(path.join(appDir, 'package.json'), basePackageJson, { spaces: 2 }); // Create basic Next.js structure await fs.ensureDir(path.join(appDir, 'app')); await fs.ensureDir(path.join(appDir, 'public')); // Create a basic page.tsx // Create a basic page.tsx - use showcase page if components are selected let pageTemplate; if (features.installComponents) { pageTemplate = templates.componentsShowcasePage; } else { pageTemplate = templates.pageContent(appName); } await fs.writeFile(path.join(appDir, 'app', 'page.tsx'), pageTemplate); // Create a basic layout.tsx const layoutContent = templates.layoutContent; await fs.writeFile(path.join(appDir, 'app', 'layout.tsx'), layoutContent); // Create minimal config files const nextConfigContent = templates.nextConfigContent; await fs.writeFile(path.join(appDir, 'next.config.js'), nextConfigContent); const tsConfigContent = templates.tsConfigContent; await fs.writeFile(path.join(appDir, 'tsconfig.json'), tsConfigContent); // Create PostCSS config const postcssConfigContent = templates.postcssConfigContent; await fs.writeFile(path.join(appDir, 'postcss.config.mjs'), postcssConfigContent); // Create the CSS file const globalCssContent = templates.globalCssContent; await fs.ensureDir(path.join(appDir, 'app')); await fs.writeFile(path.join(appDir, 'app', 'globals.css'), globalCssContent); createAppSpinner.succeed(chalk.green(`Created basic Next.js app structure manually`)); console.log(boxen( `${chalk.bold(runGradient('Manual Setup Complete'))}\n\n` + `${chalk.cyan('Basic Next.js app structure created. You may need to run:')} \n\n` + `${chalk.dim(`${packageManager} install`)} \n\n` + `${chalk.cyan('to complete the setup after installation.')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'yellow', title: '📦 Manual Setup', titleAlignment: 'center' } )); } } catch (error) { createAppSpinner.fail(chalk.red(`Failed to create Next.js app: ${error.message}`)); // Show error details in a box console.error(boxen( chalk.red(`Error creating Next.js app:\n\n${error.message}\n\n${error.stderr || ''}`), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'red', title: '❌ Error', titleAlignment: 'center' } )); // Ask if the user wants to try the manual approach instead const { manualSetup } = await inquirer.prompt([ { type: 'confirm', name: 'manualSetup', message: 'Would you like to try setting up the project manually?', default: true } ]); if (manualSetup) { createAppSpinner.text = chalk.cyan(`Setting up project manually...`); try { // Create the base directory await fs.ensureDir(appDir); // Create package.json const basePackageJson = { name: appName, version: "0.1.0", private: true, scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" }, dependencies: { next: "latest", react: "latest", "react-dom": "latest" }, devDependencies: { typescript: "latest", "@types/node": "latest", "@types/react": "latest", "@types/react-dom": "latest", "@tailwindcss/postcss": "latest", "tailwindcss": "latest", "postcss": "latest", "autoprefixer": "latest", "eslint": "latest", "eslint-config-next": "latest" } }; await fs.writeJson(path.join(appDir, 'package.json'), basePackageJson, { spaces: 2 }); // Create basic Next.js structure await fs.ensureDir(path.join(appDir, 'app')); await fs.ensureDir(path.join(appDir, 'public')); // Create a basic page.tsx // Create a basic page.tsx - use showcase page if components are selected let pageTemplate; if (features.installComponents) { pageTemplate = templates.componentsShowcasePage; } else { pageTemplate = templates.pageContent(appName); } await fs.writeFile(path.join(appDir, 'app', 'page.tsx'), pageTemplate); // Create a basic layout.tsx const layoutContent = templates.layoutContent; await fs.writeFile(path.join(appDir, 'app', 'layout.tsx'), layoutContent); // Create minimal config files const nextConfigContent = templates.nextConfigContent; await fs.writeFile(path.join(appDir, 'next.config.js'), nextConfigContent); const tsConfigContent = templates.tsConfigContent; await fs.writeFile(path.join(appDir, 'tsconfig.json'), tsConfigContent); // Create PostCSS config const postcssConfigContent = templates.postcssConfigContent; await fs.writeFile(path.join(appDir, 'postcss.config.mjs'), postcssConfigContent); // Create the CSS file const globalCssContent = templates.globalCssContent; await fs.ensureDir(path.join(appDir, 'app')); await fs.writeFile(path.join(appDir, 'app', 'globals.css'), globalCssContent); createAppSpinner.succeed(chalk.green(`Created basic Next.js app structure manually`)); } catch (manualError) { createAppSpinner.fail(chalk.red(`Failed to set up project manually: ${manualError.message}`)); throw new Error("Failed to create Next.js app manually"); } } else { throw new Error("Failed to create Next.js app"); } } spinner.succeed(chalk[currentTheme.accent](`${steps[0]} complete`)); // Check if the directory exists before trying to change into it try { // Verify the project directory exists before changing to it if (!fs.existsSync(appDir)) { throw new Error(`Project directory ${appDir} was not created properly. Cannot proceed.`); } // Check if critical files exist in the directory const requiredFiles = ['package.json']; const missingFiles = requiredFiles.filter(file => !fs.existsSync(path.join(appDir, file))); if (missingFiles.length > 0) { throw new Error(`Project directory ${appDir} is missing critical files: ${missingFiles.join(', ')}. Cannot proceed.`); } // Try to change to the project directory process.chdir(appDir); console.log(chalk.green(`✓ Changed to directory: ${appDir}`)); } catch (dirError) { spinner.fail(chalk.red(`Failed to access project directory: ${dirError.message}`)); console.error(boxen( chalk.red(`There was an error accessing the project directory at ${appDir}.\n\nThis could be due to permission issues or the directory wasn't created properly.`), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'red', title: '❌ Directory Error', titleAlignment: 'center' } )); // Ask the user what they want to do const { dirAction } = await inquirer.prompt([ { type: 'list', name: 'dirAction', message: 'How would you like to proceed?', choices: [ { name: 'Create directory manually and continue', value: 'create-manual' }, { name: 'Try again from the beginning', value: 'retry' }, { name: 'Exit', value: 'exit' } ] } ]); if (dirAction === 'create-manual') { try { // Call the manual setup function to create project structure const success = await setupProjectManually(appName, appDir, packageManager); if (success) { // Verify the directory exists again before attempting to change to it if (fs.existsSync(appDir)) { process.chdir(appDir); console.log(chalk.green(`✓ Changed to directory: ${appDir}`)); } else { throw new Error(`Directory ${appDir} still cannot be accessed after manual creation.`); } } else { throw new Error(`Manual setup failed. Cannot continue.`); } } catch (createError) { console.error(chalk.red(`Failed to create directory: ${createError.message}`)); throw new Error(`Cannot continue without a valid project directory.`); } } else if (dirAction === 'retry') { return run(); } else { process.exit(1); } } // Add pnpm configuration to package.json if (packageManager === "pnpm") { try { const pkgJsonPath = path.join(process.cwd(), 'package.json'); const pkgJson = await fs.readJSON(pkgJsonPath); // Add pnpm configuration with onlyBuiltDependencies directly in package.json pkgJson.pnpm = { ...pkgJson.pnpm, onlyBuiltDependencies: [ "sharp", "@prisma/engines", "prisma", "@prisma/client", "esbuild", "next-auth", "next-themes" ] }; await fs.writeJSON(pkgJsonPath, pkgJson, { spaces: 2 }); } catch (pkgJsonError) { spinner.warn(chalk.yellow(`Could not update package.json with pnpm configuration: ${pkgJsonError.message}`)); } } // Create components and lib folders to speed up shadcn setup fs.mkdirSync("components/ui", { recursive: true }); fs.mkdirSync("lib", { recursive: true }); // Step 2: Install dependencies updateProgress(`${steps[1]}...`); const installCmd = packageManager; let installOutput = ''; // Fast mode implementation if (features.fastMode) { try { // Enable performance optimizations const { installDependenciesConcurrently, installShadcnComponentsBatch, setupFilesOptimized } = await optimizeSetup(packageManager); // Show a box to indicate fast mode installation console.log(boxen( `${chalk.bold(runGradient('Fast Mode Enabled'))}\n\n` + `${chalk.dim('Installing packages with')} ${chalk.cyan(packageManager)} ${chalk.dim('(10x faster)')}\n` + `${chalk.dim('Optimized for performance...')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: currentTheme.secondary, title: '⚡ Fast Mode', titleAlignment: 'center' } )); // Use optimized functions for installation updateProgress(`Installing dependencies (fast mode)...`); await installDependenciesConcurrently(['react', 'react-dom', 'next', 'next-themes']); spinner.succeed(chalk[currentTheme.accent](`Core dependencies installed`)); // Enhanced component installation if needed if (features.installComponents && selectedComponents && selectedComponents.length > 0) { updateProgress(`Installing Components System (fast mode)...`); console.log(boxen( `${chalk.bold(runGradient('Installing Components'))}\n\n` + `${chalk.dim('Installing:')} ${chalk.cyan(selectedComponents.length)} ${chalk.dim('components')}\n` + `${chalk.dim('Method:')} ${chalk.cyan('Optimized batch installation')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: currentTheme.secondary, title: '🧩 Components', titleAlignment: 'center' } )); // First make sure lib and components directories exist await fs.ensureDir('lib'); await fs.ensureDir('components/ui'); // Create utils.ts if it doesn't exist if (!fs.existsSync('lib/utils.ts')) { const utilsContent = templates.utilsContent; await fs.writeFile('lib/utils.ts', utilsContent); } // Create components.json configuration const componentsJson = { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true }, "aliases": { "components": "@/components", "utils": "@/lib/utils" } }; await fs.writeFile('components.json', JSON.stringify(componentsJson, null, 2)); // Install components using the optimized batch function await installShadcnComponentsBatch(selectedComponents); spinner.succeed(chalk[currentTheme.accent](`Components installed successfully`)); } // Display the fast installation results console.log(boxen( `${chalk.bold(runGradient('Fast Mode Installation Complete'))}\n\n` + `${chalk.green('✓')} ${chalk.bold('Installed with:')} ${chalk.cyan(packageManager)} (optimized)\n` + `${chalk.green('✓')} ${chalk.bold('Core dependencies:')} next, react, react-dom\n` + `${chalk.green('✓')} ${chalk.bold('UI dependencies:')} installed in batches\n`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '✅ Fast Installation Success', titleAlignment: 'center' } )); spinner.succeed(chalk[currentTheme.accent](`${steps[1]} complete (fast mode)`)); // Skip to step 3 since we've handled dependencies updateProgress(`${steps[2]}...`); } catch (fastModeError) { spinner.warn(chalk.yellow(`Fast mode installation failed, falling back to standard installation: ${fastModeError.message}`)); // Continue with standard installation below } } // Only run standard installation if fast mode is disabled or failed if (!features.fastMode || installOutput === '') { try { // Show a box to indicate installation has started console.log(boxen( `${chalk.bold(runGradient('Installing Dependencies'))}\n\n` + `${chalk.dim('Installing packages with')} ${chalk.cyan(packageManager)}\n` + `${chalk.dim('This may take a moment...')}`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: currentTheme.secondary, title: '📦 Dependencies', titleAlignment: 'center' } )); // ... rest of existing code ... // [The rest of the original installation code follows here] // Install core dependencies first const coreDeps = [ "class-variance-authority", "clsx", "tailwind-merge", "lucide-react", "tailwindcss-animate" ]; const addCmd = packageManager === "npm" ? "install" : "add"; await execa(installCmd, [addCmd, ...coreDeps], { stdio: "inherit" }); // Try installing with the selected package manager const mainDepsPromise = execa(installCmd, packageManager === "pnpm" ? ["install", "--prefer-offline"] : packageManager === "yarn" ? ["install", "--prefer-offline"] : ["install"], { stdio: "pipe" } // Capture the output instead of inheriting stdio ); // Capture the installation output mainDepsPromise.stdout.on('data', (data) => { installOutput += data.toString(); }); mainDepsPromise.stderr.on('data', (data) => { installOutput += data.toString(); }); await mainDepsPromise; // Display the installation results in a boxen box console.log(boxen( `${chalk.bold(runGradient('Dependency Installation Complete'))}\n\n` + `${chalk.green('✓')} ${chalk.bold('Installed with:')} ${chalk.cyan(packageManager)}\n` + `${chalk.green('✓')} ${chalk.bold('Core dependencies:')} next, react, react-dom\n` + `${chalk.green('✓')} ${chalk.bold('Dev dependencies:')} typescript, tailwindcss, etc.\n` + `${chalk.green('✓')} ${chalk.bold('UI dependencies:')} lucide-react\n`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green', title: '✅ Installation Success', titleAlignment: 'center' } )); } catch (installError) { // ... existing error handling code ... // If pnpm fails, fallback to npm if (packageManager === "pnpm") { spinner.warn(chalk.yellow("pnpm installation failed, falling back to npm...")); try { // Capture npm installation output const npmInstallPromise = execa("npm", ["install"], { stdio: "pipe" }); npmInstallPromise.stdout.on('data', (data) => {