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
JavaScript
#!/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) => {