diff --git a/README.md b/README.md index 30537d8..60bed6f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ - **CLI Argument for Project Name** — skip the project name prompt by passing the app name as a CLI argument. - **Package Manager Detection** — automatically detects installed package managers (`npm`, `yarn`, `pnpm`) and only prompts with available options. +- **Robust Error Handling** — graceful handling of package installation failures with retry mechanisms and helpful error messages. +- **Compatibility Validation** — warns about package compatibility issues (e.g., Shadcn UI requires Tailwind CSS). - **Next.js App Directory** — support for the new Next.js app directory. - **Custom Page Generation** — create multiple pages at once. - **Linter Support** — choose between no linter, ESLint, and Biome. diff --git a/index.js b/index.js index 94530de..0ec9431 100644 --- a/index.js +++ b/index.js @@ -1,36 +1,93 @@ #!/usr/bin/env node -import inquirer from "inquirer"; -import path from "path"; -import fs from "fs"; -import chalk from "chalk"; -import { run, deleteFolder, createFolder, deleteFile, fileExists, writeFile } from './lib/utils.js'; +import inquirer from 'inquirer'; +import path from 'path'; +import fs from 'fs'; +import chalk from 'chalk'; +import { + run, + runWithRetry, + deleteFolder, + createFolder, + deleteFile, + fileExists, + writeFile, +} from './lib/utils.js'; import { createPages, createLayout } from './lib/templates.js'; +// Function to validate package compatibility +const validatePackageCompatibility = (answers) => { + const { useShadcn, useTailwind, useTypeScript, orm } = answers; + const warnings = []; + + // Shadcn requires Tailwind + if (useShadcn && !useTailwind) { + warnings.push({ + type: 'compatibility', + message: 'Shadcn UI requires Tailwind CSS to be enabled', + suggestion: 'Consider enabling Tailwind CSS or disabling Shadcn UI', + }); + } + + // Prisma TypeScript warning + if (orm === 'prisma' && !useTypeScript) { + warnings.push({ + type: 'recommendation', + message: 'Prisma works best with TypeScript', + suggestion: 'Consider enabling TypeScript for better Prisma experience', + }); + } + + return warnings; +}; + (async () => { - const availablePackageManagers = ["npm"]; + // Validate Node.js version + const nodeVersion = process.version; + const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]); + if (majorVersion < 16) { + console.log(chalk.bold.red('Error: Node.js 16 or higher is required')); + console.log(chalk.yellow(`Current version: ${nodeVersion}`)); + console.log(chalk.yellow('Please upgrade Node.js: https://nodejs.org/')); + process.exit(1); + } + + const availablePackageManagers = ['npm']; try { - run("yarn --version", process.cwd(), true); - availablePackageManagers.push("yarn"); - } catch (error) { } + run('yarn --version', process.cwd(), true); + availablePackageManagers.push('yarn'); + } catch (error) {} try { - run("pnpm --version", process.cwd(), true); - availablePackageManagers.push("pnpm"); - } catch (error) { } + run('pnpm --version', process.cwd(), true); + availablePackageManagers.push('pnpm'); + } catch (error) {} + + // Validate at least npm is available + if (availablePackageManagers.length === 0) { + console.log(chalk.bold.red('Error: No package manager found')); + console.log(chalk.yellow('Please install npm, yarn, or pnpm')); + process.exit(1); + } const validateProjectName = (input) => { if (input !== input.toLowerCase()) { - return chalk.red.bold("Project name must be in lowercase."); + return chalk.red.bold('Project name must be in lowercase.'); } - if (input === ".") { + if (input === '.') { const files = fs.readdirSync(process.cwd()); if (files.length > 0) { - return chalk.red.bold("The current directory is not empty. Please use a different project name."); + return chalk.red.bold( + 'The current directory is not empty. Please use a different project name.' + ); } } else { if (fs.existsSync(input)) { - return chalk.red.bold(`A directory named "${chalk.white(input)}" already exists. Please use a different project name.`); + return chalk.red.bold( + `A directory named "${chalk.white( + input + )}" already exists. Please use a different project name.` + ); } } return true; @@ -40,9 +97,13 @@ import { createPages, createLayout } from './lib/templates.js'; const answers = {}; console.log(); - console.log(chalk.bold.cyan("╔═══════════════════════════════════════════╗")); - console.log(chalk.bold.cyan("║") + chalk.bold.white(" 🚀 Create Next Quick CLI Tool ") + chalk.bold.cyan(" ║")); - console.log(chalk.bold.cyan("╚═══════════════════════════════════════════╝")); + console.log(chalk.bold.cyan('╔═══════════════════════════════════════════╗')); + console.log( + chalk.bold.cyan('║') + + chalk.bold.white(' 🚀 Create Next Quick CLI Tool ') + + chalk.bold.cyan(' ║') + ); + console.log(chalk.bold.cyan('╚═══════════════════════════════════════════╝')); console.log(); if (appName) { @@ -57,199 +118,304 @@ import { createPages, createLayout } from './lib/templates.js'; if (!answers.projectName) { const appNameAnswers = await inquirer.prompt([ { - type: "input", - name: "projectName", - message: "Enter project name:", - filter: (input) => input.trim() === '' ? '.' : input.trim(), - validate: validateProjectName - } + type: 'input', + name: 'projectName', + message: 'Enter project name:', + filter: (input) => (input.trim() === '' ? '.' : input.trim()), + validate: validateProjectName, + }, ]); answers.projectName = appNameAnswers.projectName; } const otherAnswers = await inquirer.prompt([ { - type: "list", - name: "packageManager", - message: "Choose a package manager:", + type: 'list', + name: 'packageManager', + message: 'Choose a package manager:', choices: availablePackageManagers, - default: "pnpm" + default: 'pnpm', }, { - type: "confirm", - name: "useTypeScript", - message: "Do you want to use TypeScript?", - default: true + type: 'confirm', + name: 'useTypeScript', + message: 'Do you want to use TypeScript?', + default: true, }, { - type: "confirm", - name: "useTailwind", - message: "Do you want to use Tailwind CSS?", - default: true + type: 'confirm', + name: 'useTailwind', + message: 'Do you want to use Tailwind CSS?', + default: true, }, { - type: "confirm", - name: "useSrcDir", - message: "Do you want to use src directory?", - default: true + type: 'confirm', + name: 'useSrcDir', + message: 'Do you want to use src directory?', + default: true, }, { - type: "confirm", - name: "useAppDir", - message: "Do you want to use the app directory?", - default: true + type: 'confirm', + name: 'useAppDir', + message: 'Do you want to use the app directory?', + default: true, }, { - type: "input", - name: "pages", - message: "Enter pages (comma-separated, default: none):", - default: "", - filter: (input) => input.split(',').map((page) => page.trim()).filter(page => page !== '') + type: 'input', + name: 'pages', + message: 'Enter pages (comma-separated, default: none):', + default: '', + filter: (input) => + input + .split(',') + .map((page) => page.trim()) + .filter((page) => page !== ''), }, { - type: "list", - name: "linter", - message: "Choose a linter:", - choices: ["none", "eslint", "biome"], - default: "none" + type: 'list', + name: 'linter', + message: 'Choose a linter:', + choices: ['none', 'eslint', 'biome'], + default: 'none', }, { - type: "list", - name: "orm", - message: "Choose an ORM:", - choices: ["none", "prisma", "drizzle"], - default: "none" + type: 'list', + name: 'orm', + message: 'Choose an ORM:', + choices: ['none', 'prisma', 'drizzle'], + default: 'none', }, { - type: "confirm", - name: "useShadcn", - message: "Do you want to use Shadcn UI?", - default: false - } + type: 'confirm', + name: 'useShadcn', + message: 'Do you want to use Shadcn UI?', + default: false, + }, ]); Object.assign(answers, otherAnswers); - const { projectName, packageManager, useTypeScript, useTailwind, useAppDir, useSrcDir, pages, linter, orm, useShadcn } = answers; + // Validate package compatibility + const warnings = validatePackageCompatibility(answers); + if (warnings.length > 0) { + console.log(); + console.log(chalk.bold.yellow('⚠️ Compatibility Warnings:')); + warnings.forEach((warning) => { + console.log(chalk.yellow(`• ${warning.message}`)); + console.log(chalk.gray(` ${warning.suggestion}`)); + }); + console.log(); + } + + const { + projectName, + packageManager, + useTypeScript, + useTailwind, + useAppDir, + useSrcDir, + pages, + linter, + orm, + useShadcn, + } = answers; const projectPath = path.join(process.cwd(), projectName); console.log(); - console.log(chalk.bold.hex("#23f0bcff")("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - console.log(chalk.bold.white(` Creating project: ${chalk.cyan(projectName)}`)); - console.log(chalk.bold.hex("#23f0bcff")("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + console.log( + chalk.bold.hex('#23f0bcff')('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + ); + console.log( + chalk.bold.white(` Creating project: ${chalk.cyan(projectName)}`) + ); + console.log( + chalk.bold.hex('#23f0bcff')('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + ); console.log(); let command = `npx --yes create-next-app@latest ${projectName} --use-${packageManager} --yes`; if (useTypeScript) { - command += " --ts"; + command += ' --ts'; } else { - command += " --js"; + command += ' --js'; } if (useTailwind) { - command += " --tailwind"; + command += ' --tailwind'; } if (useSrcDir) { - command += " --src-dir"; + command += ' --src-dir'; } if (useAppDir) { - command += " --app"; + command += ' --app'; } else { - command += " --no-app"; + command += ' --no-app'; } - if (linter === "none") { - command += " --no-eslint"; + if (linter === 'none') { + command += ' --no-eslint'; } - console.log(chalk.cyan(`Installing dependencies with ${chalk.bold(packageManager)}...`)); + console.log( + chalk.cyan(`Installing dependencies with ${chalk.bold(packageManager)}...`) + ); try { - run(command); - console.log(chalk.bold.green("Dependencies installed successfully")); + await runWithRetry(command); + console.log(chalk.bold.green('Dependencies installed successfully')); } catch (err) { - console.log(chalk.bold.red("Failed to install dependencies")); - process.exit(1); - } + console.log(chalk.bold.red('Failed to install dependencies')); + console.log(chalk.red(`Error: ${err.message}`)); - console.log(chalk.yellow("Cleaning up default files...")); - - if (!useAppDir) { - const apiHelloPath = useSrcDir - ? path.join(projectPath, "src", "pages", "api", "hello.js") - : path.join(projectPath, "pages", "api", "hello.js"); - if (fileExists(apiHelloPath)) { - deleteFile(apiHelloPath); + // Clean up failed project directory + if (fs.existsSync(projectPath)) { + console.log(chalk.yellow('Cleaning up failed installation...')); + deleteFolder(projectPath); } - } - - const publicPath = path.join(projectPath, "public"); - deleteFolder(publicPath); - createFolder(publicPath); - console.log(chalk.bold.green("Cleanup complete")); + console.log(chalk.yellow('\nTroubleshooting tips:')); + console.log(chalk.white('• Check your internet connection')); + console.log(chalk.white('• Verify Node.js and npm are properly installed')); + console.log(chalk.white('• Try running the command manually:')); + console.log(chalk.gray(` ${command}`)); + process.exit(1); + } - console.log(chalk.magenta("Creating layout files...")); + console.log(chalk.yellow('Cleaning up default files...')); - createLayout(projectPath, projectName, useTypeScript, useAppDir, useSrcDir); + try { + if (!useAppDir) { + const apiHelloPath = useSrcDir + ? path.join(projectPath, 'src', 'pages', 'api', 'hello.js') + : path.join(projectPath, 'pages', 'api', 'hello.js'); + if (fileExists(apiHelloPath)) { + deleteFile(apiHelloPath); + } + } - const pagesPath = useAppDir - ? (useSrcDir ? path.join(projectPath, "src", "app") : path.join(projectPath, "app")) - : (useSrcDir ? path.join(projectPath, "src", "pages") : path.join(projectPath, "pages")); + const publicPath = path.join(projectPath, 'public'); + deleteFolder(publicPath); + createFolder(publicPath); - createPages(pagesPath, pages, useTypeScript, useAppDir, useSrcDir); + console.log(chalk.bold.green('Cleanup complete')); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Some cleanup operations failed')); + console.log(chalk.red(`Error: ${error.message}`)); + } - const faviconPathInAppOrSrc = useAppDir - ? (useSrcDir ? path.join(projectPath, "src", "app", "favicon.ico") : path.join(projectPath, "app", "favicon.ico")) - : (useSrcDir ? path.join(projectPath, "src", "favicon.ico") : path.join(projectPath, "favicon.ico")); + console.log(chalk.magenta('Creating layout files...')); - if (fileExists(faviconPathInAppOrSrc)) { - deleteFile(faviconPathInAppOrSrc); - } + try { + createLayout(projectPath, projectName, useTypeScript, useAppDir, useSrcDir); + + const pagesPath = useAppDir + ? useSrcDir + ? path.join(projectPath, 'src', 'app') + : path.join(projectPath, 'app') + : useSrcDir + ? path.join(projectPath, 'src', 'pages') + : path.join(projectPath, 'pages'); + + createPages(pagesPath, pages, useTypeScript, useAppDir, useSrcDir); + + const faviconPathInAppOrSrc = useAppDir + ? useSrcDir + ? path.join(projectPath, 'src', 'app', 'favicon.ico') + : path.join(projectPath, 'app', 'favicon.ico') + : useSrcDir + ? path.join(projectPath, 'src', 'favicon.ico') + : path.join(projectPath, 'favicon.ico'); + + if (fileExists(faviconPathInAppOrSrc)) { + deleteFile(faviconPathInAppOrSrc); + } - let defaultPagePath; - if (useAppDir) { - defaultPagePath = useSrcDir - ? path.join(projectPath, "src", "app", useTypeScript ? "page.tsx" : "page.js") - : path.join(projectPath, "app", useTypeScript ? "page.tsx" : "page.js"); - } else { - defaultPagePath = useSrcDir - ? path.join(projectPath, "src", "pages", useTypeScript ? "index.tsx" : "index.js") - : path.join(projectPath, "pages", useTypeScript ? "index.tsx" : "index.js"); - } + let defaultPagePath; + if (useAppDir) { + defaultPagePath = useSrcDir + ? path.join( + projectPath, + 'src', + 'app', + useTypeScript ? 'page.tsx' : 'page.js' + ) + : path.join(projectPath, 'app', useTypeScript ? 'page.tsx' : 'page.js'); + } else { + defaultPagePath = useSrcDir + ? path.join( + projectPath, + 'src', + 'pages', + useTypeScript ? 'index.tsx' : 'index.js' + ) + : path.join( + projectPath, + 'pages', + useTypeScript ? 'index.tsx' : 'index.js' + ); + } - const emptyPageContent = `export default function Page() { + const emptyPageContent = `export default function Page() { return ( <> ); }`; - writeFile(defaultPagePath, emptyPageContent); - - const readmePath = path.join(projectPath, "README.md"); - writeFile(readmePath, `# ${projectName}`); + writeFile(defaultPagePath, emptyPageContent); - console.log(chalk.bold.green("Layout and pages created")); + const readmePath = path.join(projectPath, 'README.md'); + writeFile(readmePath, `# ${projectName}`); - if (linter === "biome") { - console.log(chalk.blue("Setting up Biome linter...")); - - run(`${packageManager} install --save-dev @biomejs/biome`, projectPath); - run(`npx @biomejs/biome init`, projectPath); + console.log(chalk.bold.green('Layout and pages created')); + } catch (error) { + console.log( + chalk.bold.yellow('Warning: Failed to create some layout files') + ); + console.log(chalk.red(`Error: ${error.message}`)); + } - console.log(chalk.bold.green("Biome linter configured")); + if (linter === 'biome') { + console.log(chalk.blue('Setting up Biome linter...')); + + try { + await runWithRetry( + `${packageManager} install --save-dev @biomejs/biome`, + projectPath + ); + await runWithRetry(`npx @biomejs/biome init`, projectPath); + console.log(chalk.bold.green('Biome linter configured')); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Failed to set up Biome linter')); + console.log(chalk.red(`Error: ${error.message}`)); + console.log( + chalk.yellow('You can manually install Biome later by running:') + ); + console.log( + chalk.gray(` ${packageManager} install --save-dev @biomejs/biome`) + ); + console.log(chalk.gray(` npx @biomejs/biome init`)); + } } - if (orm === "prisma") { - console.log(chalk.blue("Setting up Prisma ORM...")); + if (orm === 'prisma') { + console.log(chalk.blue('Setting up Prisma ORM...')); - run(`${packageManager} install --save-dev prisma`, projectPath); - run(`${packageManager} install @prisma/client`, projectPath); - run(`npx prisma init`, projectPath); + try { + await runWithRetry( + `${packageManager} install --save-dev prisma`, + projectPath + ); + await runWithRetry( + `${packageManager} install @prisma/client`, + projectPath + ); + await runWithRetry(`npx prisma init`, projectPath); - const prismaLibDir = useSrcDir ? path.join(projectPath, "src", "lib") : path.join(projectPath, "lib"); - createFolder(prismaLibDir); + const prismaLibDir = useSrcDir + ? path.join(projectPath, 'src', 'lib') + : path.join(projectPath, 'lib'); + createFolder(prismaLibDir); - const prismaContent = `import { PrismaClient } from '@prisma/client' + const prismaContent = `import { PrismaClient } from '@prisma/client' declare global { var prisma: PrismaClient | undefined @@ -260,18 +426,35 @@ import { createPages, createLayout } from './lib/templates.js'; if (process.env.NODE_ENV !== 'production') global.prisma = prisma export default prisma;`; - writeFile(path.join(prismaLibDir, "prisma.ts"), prismaContent); - - console.log(chalk.bold.green("Prisma ORM configured")); + writeFile(path.join(prismaLibDir, 'prisma.ts'), prismaContent); + + console.log(chalk.bold.green('Prisma ORM configured')); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Failed to set up Prisma ORM')); + console.log(chalk.red(`Error: ${error.message}`)); + console.log( + chalk.yellow('You can manually install Prisma later by running:') + ); + console.log(chalk.gray(` ${packageManager} install --save-dev prisma`)); + console.log(chalk.gray(` ${packageManager} install @prisma/client`)); + console.log(chalk.gray(` npx prisma init`)); + } } - if (orm === "drizzle") { - console.log(chalk.blue("Setting up Drizzle ORM...")); + if (orm === 'drizzle') { + console.log(chalk.blue('Setting up Drizzle ORM...')); - run(`${packageManager} install drizzle-orm @vercel/postgres`, projectPath); - run(`${packageManager} install --save-dev drizzle-kit`, projectPath); + try { + await runWithRetry( + `${packageManager} install drizzle-orm @vercel/postgres`, + projectPath + ); + await runWithRetry( + `${packageManager} install --save-dev drizzle-kit`, + projectPath + ); - const drizzleConfigContent = `import type { Config } from 'drizzle-kit'; + const drizzleConfigContent = `import type { Config } from 'drizzle-kit'; export default { schema: './src/db/schema.ts', @@ -281,63 +464,148 @@ import { createPages, createLayout } from './lib/templates.js'; connectionString: process.env.DATABASE_URL!, }, } satisfies Config;`; - writeFile(path.join(projectPath, "drizzle.config.ts"), drizzleConfigContent); + writeFile( + path.join(projectPath, 'drizzle.config.ts'), + drizzleConfigContent + ); - const dbDir = useSrcDir ? path.join(projectPath, "src", "db") : path.join(projectPath, "db"); - createFolder(dbDir); + const dbDir = useSrcDir + ? path.join(projectPath, 'src', 'db') + : path.join(projectPath, 'db'); + createFolder(dbDir); - const schemaContent = `import { pgTable, serial, text } from 'drizzle-orm/pg-core'; + const schemaContent = `import { pgTable, serial, text } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), });`; - writeFile(path.join(dbDir, "schema.ts"), schemaContent); - - console.log(chalk.bold.green("Drizzle ORM configured")); + writeFile(path.join(dbDir, 'schema.ts'), schemaContent); + + console.log(chalk.bold.green('Drizzle ORM configured')); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Failed to set up Drizzle ORM')); + console.log(chalk.red(`Error: ${error.message}`)); + console.log( + chalk.yellow('You can manually install Drizzle later by running:') + ); + console.log( + chalk.gray(` ${packageManager} install drizzle-orm @vercel/postgres`) + ); + console.log( + chalk.gray(` ${packageManager} install --save-dev drizzle-kit`) + ); + } } if (useShadcn) { - console.log(chalk.magenta("Setting up Shadcn UI...")); - - run(`${packageManager} install --save-dev tailwindcss-animate class-variance-authority`, projectPath); - run(`npx shadcn@latest init`, projectPath); - - const componentsJsonPath = path.join(projectPath, "components.json"); - const componentsJsonContent = { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": useAppDir, - "tsx": useTypeScript, - "tailwind": { - "config": useTypeScript ? "tailwind.config.ts" : "tailwind.config.js", - "css": useAppDir - ? (useSrcDir ? "src/app/globals.css" : "app/globals.css") - : (useSrcDir ? "src/styles/globals.css" : "styles/globals.css"), - "baseColor": "slate", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils" + console.log(chalk.magenta('Setting up Shadcn UI...')); + + try { + // Check if Tailwind is enabled for compatibility + if (!useTailwind) { + console.log( + chalk.bold.yellow('Warning: Shadcn UI requires Tailwind CSS') + ); + console.log( + chalk.yellow( + 'Skipping Shadcn UI setup. Please enable Tailwind CSS to use Shadcn UI.' + ) + ); + } else { + await runWithRetry( + `${packageManager} install --save-dev tailwindcss-animate class-variance-authority clsx tailwind-merge`, + projectPath + ); + + // Create components.json configuration + const componentsJsonPath = path.join(projectPath, 'components.json'); + const componentsJsonContent = { + $schema: 'https://ui.shadcn.com/schema.json', + style: 'default', + rsc: useAppDir, + tsx: useTypeScript, + tailwind: { + config: useTypeScript ? 'tailwind.config.ts' : 'tailwind.config.js', + css: useAppDir + ? useSrcDir + ? 'src/app/globals.css' + : 'app/globals.css' + : useSrcDir + ? 'src/styles/globals.css' + : 'styles/globals.css', + baseColor: 'slate', + cssVariables: true, + }, + aliases: { + components: '@/components', + utils: '@/lib/utils', + }, + }; + writeFile( + componentsJsonPath, + JSON.stringify(componentsJsonContent, null, 2) + ); + + // Create lib/utils.ts for Shadcn + const libDir = useSrcDir + ? path.join(projectPath, 'src', 'lib') + : path.join(projectPath, 'lib'); + createFolder(libDir); + + const utilsContent = `import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +}`; + const utilsFilename = useTypeScript ? 'utils.ts' : 'utils.js'; + writeFile(path.join(libDir, utilsFilename), utilsContent); + + console.log(chalk.bold.green('Shadcn UI configured')); + console.log( + chalk.cyan( + 'You can now add components using: npx shadcn@latest add ' + ) + ); } - }; - writeFile(componentsJsonPath, JSON.stringify(componentsJsonContent, null, 2)); - - console.log(chalk.bold.green("Shadcn UI configured")); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Failed to set up Shadcn UI')); + console.log(chalk.red(`Error: ${error.message}`)); + console.log( + chalk.yellow('You can manually install Shadcn UI later by running:') + ); + console.log( + chalk.gray( + ` ${packageManager} install --save-dev tailwindcss-animate class-variance-authority clsx tailwind-merge` + ) + ); + console.log(chalk.gray(` npx shadcn@latest init`)); + } } - if (orm !== "none") { - const envContent = `DATABASE_URL="your_db_url"`; - writeFile(path.join(projectPath, ".env"), envContent); + if (orm !== 'none') { + try { + const envContent = `DATABASE_URL="your_db_url"`; + writeFile(path.join(projectPath, '.env'), envContent); + } catch (error) { + console.log(chalk.bold.yellow('Warning: Failed to create .env file')); + console.log( + chalk.yellow('Please manually create a .env file with DATABASE_URL') + ); + } } console.log(); - console.log(chalk.bold.hex("#23f0bcff")("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); - console.log(chalk.bold.white(" Setup complete!")); - console.log(chalk.bold.hex("#23f0bcff")("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")); + console.log( + chalk.bold.hex('#23f0bcff')('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + ); + console.log(chalk.bold.white(' Setup complete!')); + console.log( + chalk.bold.hex('#23f0bcff')('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + ); console.log(); - console.log(chalk.bold.white("-> Next steps:")); + console.log(chalk.bold.white('-> Next steps:')); console.log(chalk.cyan(` cd ${chalk.bold.white(projectName)}`)); console.log(chalk.cyan(` ${packageManager} ${chalk.bold.white(`run dev`)}`)); console.log(); diff --git a/lib/utils.js b/lib/utils.js index 1ffa79f..98cbec2 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,42 +1,124 @@ -import { execSync } from "child_process"; -import fs from "fs"; -import path from "path"; +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; export const run = (cmd, cwd = process.cwd(), silent = false) => { - if (!silent) { - console.log(`\nRunning: ${cmd}`); + if (!silent) { + console.log(`\nRunning: ${cmd}`); + } + try { + execSync(cmd, { stdio: silent ? 'pipe' : 'inherit', cwd }); + } catch (error) { + throw new Error(`Command failed: ${cmd}\nError: ${error.message}`); + } +}; + +export const runWithRetry = async ( + cmd, + cwd = process.cwd(), + silent = false, + maxRetries = 2 +) => { + let lastError; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + if (!silent) { + console.log( + `\n${attempt > 1 ? `Retry ${attempt - 1}: ` : ''}Running: ${cmd}` + ); + } + execSync(cmd, { stdio: silent ? 'pipe' : 'inherit', cwd }); + return; // Success + } catch (error) { + lastError = error; + if (attempt < maxRetries) { + console.log(`\nAttempt ${attempt} failed, retrying...`); + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second before retry + } } - execSync(cmd, { stdio: silent ? "pipe" : "inherit", cwd }); + } + + throw new Error( + `Command failed after ${maxRetries} attempts: ${cmd}\nError: ${lastError.message}` + ); }; export const writeFile = (filePath, content) => { + try { const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, content); + } catch (error) { + throw new Error(`Failed to write file ${filePath}: ${error.message}`); + } }; export const readFile = (filePath) => { - return fs.readFileSync(filePath, "utf-8"); + return fs.readFileSync(filePath, 'utf-8'); }; export const fileExists = (filePath) => { - return fs.existsSync(filePath); + return fs.existsSync(filePath); }; export const createFolder = (folderPath) => { + try { fs.mkdirSync(folderPath, { recursive: true }); + } catch (error) { + throw new Error(`Failed to create folder ${folderPath}: ${error.message}`); + } }; export const deleteFile = (filePath) => { - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } }; export const deleteFolder = (folderPath) => { - if (fs.existsSync(folderPath)) { - fs.rmSync(folderPath, { recursive: true, force: true }); - } -}; \ No newline at end of file + if (fs.existsSync(folderPath)) { + fs.rmSync(folderPath, { recursive: true, force: true }); + } +}; + +export const checkPackageAvailability = async ( + packageName, + packageManager = 'npm' +) => { + try { + const command = + packageManager === 'npm' + ? `npm view ${packageName} version` + : packageManager === 'yarn' + ? `yarn info ${packageName} version` + : `pnpm view ${packageName} version`; + + execSync(command, { stdio: 'pipe' }); + return true; + } catch (error) { + return false; + } +}; + +export const validateEnvironment = () => { + const errors = []; + + // Check Node.js version + const nodeVersion = process.version; + const majorVersion = parseInt(nodeVersion.slice(1).split('.')[0]); + if (majorVersion < 16) { + errors.push(`Node.js 16+ required (current: ${nodeVersion})`); + } + + // Check if npm is available + try { + execSync('npm --version', { stdio: 'pipe' }); + } catch (error) { + errors.push('npm is not installed or not in PATH'); + } + + return errors; +}; diff --git a/test/manual-error-test.js b/test/manual-error-test.js new file mode 100644 index 0000000..3bda395 --- /dev/null +++ b/test/manual-error-test.js @@ -0,0 +1,129 @@ +#!/usr/bin/env node + +// Comprehensive test for create-next-quick error handling +import path from 'path'; +import fs from 'fs'; +import chalk from 'chalk'; +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const cliPath = path.join(__dirname, '..', 'index.js'); + +console.log(chalk.bold.cyan('🧪 Testing create-next-quick error handling')); +console.log( + chalk.gray('This will test various error scenarios and recovery mechanisms\n') +); + +async function runCLITest(projectName, inputs, expectSuccess = true) { + return new Promise((resolve) => { + console.log(chalk.blue(`Testing: ${projectName}`)); + + const child = spawn('node', [cliPath, projectName]); + let output = ''; + let errorOutput = ''; + + child.stdout.on('data', (data) => { + output += data.toString(); + }); + + child.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + // Send inputs + let inputIndex = 0; + const sendInput = () => { + if (inputIndex < inputs.length) { + setTimeout(() => { + child.stdin.write(inputs[inputIndex] + '\n'); + inputIndex++; + sendInput(); + }, 500); + } else { + setTimeout(() => { + child.stdin.end(); + }, 1000); + } + }; + sendInput(); + + child.on('close', (code) => { + const projectPath = path.join('/tmp', projectName); + + // Clean up + if (fs.existsSync(projectPath)) { + fs.rmSync(projectPath, { recursive: true, force: true }); + } + + if (expectSuccess && code === 0) { + console.log(chalk.green(`✅ ${projectName}: Passed`)); + } else if (!expectSuccess && code !== 0) { + console.log(chalk.green(`✅ ${projectName}: Failed as expected`)); + } else { + console.log( + chalk.red(`❌ ${projectName}: Unexpected result (code: ${code})`) + ); + if (errorOutput) { + console.log( + chalk.gray('Error output:'), + errorOutput.substring(0, 200) + ); + } + } + + resolve({ code, output, errorOutput }); + }); + + // Handle timeout + setTimeout(() => { + child.kill(); + console.log(chalk.yellow(`⏰ ${projectName}: Timed out`)); + resolve({ code: -1, output, errorOutput }); + }, 30000); // 30 second timeout + }); +} + +async function runTests() { + const tests = [ + { + name: 'test-basic-npm', + inputs: ['npm', 'n', 'n', 'y', 'y', '', 'none', 'none', 'n'], + expectSuccess: true, + }, + { + name: 'test-with-prisma', + inputs: ['npm', 'y', 'y', 'y', 'y', '', 'none', 'prisma', 'n'], + expectSuccess: true, + }, + { + name: 'test-shadcn-without-tailwind', + inputs: ['npm', 'y', 'n', 'y', 'y', '', 'none', 'none', 'y'], + expectSuccess: true, // Should work with warnings + }, + ]; + + console.log( + chalk.yellow( + 'Note: These tests will create and delete temporary projects in /tmp\n' + ) + ); + + for (const test of tests) { + try { + await runCLITest(test.name, test.inputs, test.expectSuccess); + } catch (error) { + console.log( + chalk.red(`❌ ${test.name}: Error running test - ${error.message}`) + ); + } + console.log(''); // spacing + } + + console.log(chalk.bold.green('🎉 Error handling tests completed!')); +} + +// Change to /tmp directory for testing +process.chdir('/tmp'); +runTests().catch(console.error); diff --git a/test/test-error-handling.js b/test/test-error-handling.js new file mode 100644 index 0000000..44d3f0d --- /dev/null +++ b/test/test-error-handling.js @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +import { run, runWithRetry } from '../lib/utils.js'; +import chalk from 'chalk'; + +async function testErrorHandling() { + console.log(chalk.bold.cyan('Testing error handling improvements...')); + + console.log('\n1. Testing invalid command handling...'); + try { + run('invalid-command-that-does-not-exist', process.cwd(), true); + console.log(chalk.red('❌ Should have thrown an error')); + } catch (error) { + console.log( + chalk.green('✅ Error correctly caught:'), + error.message.substring(0, 50) + '...' + ); + } + + console.log('\n2. Testing retry mechanism...'); + try { + await runWithRetry( + 'invalid-command-that-does-not-exist', + process.cwd(), + true, + 1 + ); + console.log(chalk.red('❌ Should have thrown an error')); + } catch (error) { + console.log( + chalk.green('✅ Retry mechanism works:'), + 'Failed after 1 attempt' + ); + } + + console.log('\n' + chalk.bold.green('Error handling tests completed!')); +} + +testErrorHandling().catch(console.error);