UNPKG

typescript-express-starter

Version:
696 lines (608 loc) โ€ข 23.3 kB
#!/usr/bin/env node /***************************************************************** * TYPESCRIPT-EXPRESS-STARTER - Quick and Easy TypeScript Express Starter * (c) 2020-present AGUMON (https://github.com/ljlm0402/typescript-express-starter) * * MIT License * * Made with โค๏ธ by AGUMON ๐Ÿฆ– *****************************************************************/ import { select, text, isCancel, intro, outro, cancel, note, confirm } from '@clack/prompts'; import chalk from 'chalk'; import editJsonFile from 'edit-json-file'; import { execa } from 'execa'; import fs from 'fs-extra'; import ora from 'ora'; import path from 'path'; // Config and constants import { CONFIG, getEnvironmentConfig } from './config.js'; import { PACKAGE_MANAGER, TEMPLATES_VALUES, DEVTOOLS_VALUES } from './common.js'; import { getBenchmarkInfo } from './presets.js'; // Database configuration import { TEMPLATE_DB, generateDockerFiles, generateDockerCompose } from './db-map.js'; // Error handling import { CLIError, NetworkError, FileSystemError, printError } from './errors.js'; // Validation utilities import { validateProjectName, validateProjectPath, sanitizeInput, validateNodeVersion, validateAllTemplates, } from './validators.js'; // Performance optimizations import { versionCache, PackageBatch } from './performance.js'; // AST utilities import { injectSwaggerIntoApp } from './ast-utils.js'; const RECOMMENDED_TOOLS = { Linter: 'biome', Compiler: 'tsup', Testing: 'vitest', Infrastructure: null, }; function isNewerVersion(latest, current) { const normalize = (version) => String(version) .replace(/^v/i, '') .split('-')[0] .split('.') .map((part) => Number.parseInt(part, 10) || 0); const latestParts = normalize(latest); const currentParts = normalize(current); const maxLength = Math.max(latestParts.length, currentParts.length); for (let index = 0; index < maxLength; index += 1) { const left = latestParts[index] ?? 0; const right = currentParts[index] ?? 0; if (left > right) return true; if (left < right) return false; } return false; } function getGroupedDevtools() { return DEVTOOLS_VALUES.reduce((acc, tool) => { const category = tool.category || 'Others'; if (!acc[category]) acc[category] = []; acc[category].push(tool); return acc; }, {}); } function getPresetDevtools(setupProfile, groupedDevtools) { if (setupProfile === 'minimal') return []; const picked = []; for (const [category, tools] of Object.entries(groupedDevtools)) { if (setupProfile === 'full') { const firstTool = tools[0]; if (firstTool) picked.push(firstTool.value); continue; } const recommendedValue = RECOMMENDED_TOOLS[category]; const recommendedTool = tools.find((tool) => tool.value === recommendedValue) || tools[0]; if (recommendedTool) picked.push(recommendedTool.value); } return picked; } function buildSetupSummary({ pkgManager, template, projectName, devtoolValues, setupMode }) { const selectedTemplate = TEMPLATES_VALUES.find((item) => item.value === template); const selectedToolNames = devtoolValues .map((value) => DEVTOOLS_VALUES.find((tool) => tool.value === value)?.name) .filter(Boolean); return [ `Setup mode : ${setupMode}`, `Package manager: ${pkgManager}`, `Template : ${selectedTemplate?.name || template}`, `Project name : ${projectName}`, `Devtools : ${selectedToolNames.length > 0 ? selectedToolNames.join(', ') : 'None'}`, ].join('\n'); } // ========== [๊ณตํ†ต ํ•จ์ˆ˜๋“ค] ========== // ์ตœ์‹  CLI ๋ฒ„์ „ ์ฒดํฌ & ์„ ํƒ์  ์„ค์น˜ async function checkForUpdate() { try { const pkgPath = path.resolve(process.cwd(), 'package.json'); const localPkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); const pkgName = localPkg.name || 'typescript-express-stater'; const localVersion = localPkg.version || '0.0.0'; const latest = await versionCache.getLatestVersion(pkgName); if (isNewerVersion(latest, localVersion)) { console.log( chalk.yellow(`๐Ÿ”” New version available: ${latest} (You are on ${localVersion})`), ); const shouldUpdate = await confirm({ message: `Do you want to update ${pkgName} to version ${latest}?`, initial: true, }); if (isCancel(shouldUpdate)) return; if (shouldUpdate) { console.log(chalk.gray(` Updating to latest version...`)); try { await execa('npm', ['install', '-g', `${pkgName}@${latest}`], { stdio: 'inherit' }); console.log(chalk.green(` โœ“ Updated ${pkgName} to ${latest}`)); } catch (err) { printError(new NetworkError(`Failed to update ${pkgName}`, 'update', err.message)); } } else { console.log(chalk.gray('Skipped updating.')); } } } catch (err) { printError(new NetworkError('Failed to check latest version', 'version-check', err.message)); } } // ํŒจํ‚ค์ง€๋งค๋‹ˆ์ € ๊ธ€๋กœ๋ฒŒ ์„ค์น˜์—ฌ๋ถ€ async function checkPkgManagerInstalled(pm) { try { await execa(pm, ['--version']); return true; } catch { return false; } } // ๋„๊ตฌ ๊ฐ„ ์˜์กด์„ฑ ์ž๋™ ํ•ด๊ฒฐ function resolveDependencies(selected) { const all = new Set(selected); let changed = true; while (changed) { changed = false; for (const tool of DEVTOOLS_VALUES) { if (all.has(tool.value) && tool.requires) { for (const req of tool.requires) { if (!all.has(req)) { all.add(req); changed = true; } } } } } return Array.from(all); } // ํŒŒ์ผ ๋ณต์‚ฌ async function copyDevtoolFiles(devtool, destDir, template = 'default') { // ๋„๊ตฌ ์นดํ…Œ๊ณ ๋ฆฌ์— ๋”ฐ๋ฅธ ๊ฒฝ๋กœ ๋งคํ•‘ const categoryPathMap = { Linter: 'core', Compiler: 'build', Testing: 'test', 'API development': 'api', Infrastructure: 'infrastructure', 'Git Tools': 'core', Deployment: 'core', }; // ํ…œํ”Œ๋ฆฟ ์ด๋ฆ„์„ devtools ํŒŒ์ผ๋ช… prefix๋กœ ๋งคํ•‘ const templateToPrefix = { 'drizzle-postgresql': 'drizzle', 'prisma-postgresql': 'prisma', 'mongoose-mongodb': 'mongoose', 'typegoose-mongodb': 'typegoose', default: null, }; // ํ…œํ”Œ๋ฆฟ๋ณ„ src ํด๋” ๋งคํ•‘ (Testing ์นดํ…Œ๊ณ ๋ฆฌ์šฉ) const templateToSrcFolder = { 'drizzle-postgresql': 'src-drizzle', 'prisma-postgresql': 'src-prisma', // 'mongoose-mongodb': 'src-default', // 'typegoose-mongodb': 'src-default', default: 'src-default', }; const categoryPath = categoryPathMap[devtool.category] || ''; for (const file of devtool.files) { let src = path.join(CONFIG.paths.devtools, categoryPath, devtool.value, file); const dst = path.join(destDir, file); // Testing ์นดํ…Œ๊ณ ๋ฆฌ์˜ src/test ํŒŒ์ผ ํŠน๋ณ„ ์ฒ˜๋ฆฌ if (devtool.category === 'Testing' && (file === 'src/test' || file.startsWith('src/'))) { const srcFolderName = templateToSrcFolder[template] || 'src-default'; src = path.join(CONFIG.paths.devtools, categoryPath, devtool.value, srcFolderName, 'test'); if (await fs.pathExists(src)) { console.log(chalk.cyan(` โœ“ Using template-specific test files: ${srcFolderName}/test`)); } else { // fallback to src-default if template-specific src doesn't exist src = path.join(CONFIG.paths.devtools, categoryPath, devtool.value, 'src-default', 'test'); console.log(chalk.yellow(` โ†ป Falling back to src-default/test`)); } // src/test ํด๋”๋ฅผ ๋ณต์‚ฌ if (await fs.pathExists(src)) { await fs.copy(src, dst, { overwrite: true }); console.log(chalk.gray(` โ€— ${file} copied from ${srcFolderName}.`)); } else { console.log(chalk.red(` โœ— ${file} not found at ${src}`)); } continue; } // Testing ์นดํ…Œ๊ณ ๋ฆฌ์˜ ์„ค์ • ํŒŒ์ผ ํŠน๋ณ„ ์ฒ˜๋ฆฌ (์ƒˆ๋กœ์šด ๊ตฌ์กฐ: src-{template}/์„ค์ •ํŒŒ์ผ) if (devtool.category === 'Testing' && file.includes('.config.')) { const srcFolderName = templateToSrcFolder[template] || 'src-default'; src = path.join(CONFIG.paths.devtools, categoryPath, devtool.value, srcFolderName, file); if (await fs.pathExists(src)) { console.log(chalk.cyan(` โœ“ Using template-specific config: ${srcFolderName}/${file}`)); } else { // fallback to src-default if template-specific config doesn't exist src = path.join(CONFIG.paths.devtools, categoryPath, devtool.value, 'src-default', file); console.log(chalk.yellow(` โ†ป Falling back to src-default/${file}`)); } if (await fs.pathExists(src)) { await fs.copy(src, dst, { overwrite: true }); console.log(chalk.gray(` โ€— ${file} copied from ${srcFolderName}.`)); } else { console.log(chalk.red(` โœ— ${file} not found at ${src}`)); } continue; } // ํ…œํ”Œ๋ฆฟ๋ณ„ ํŠนํ™” ์„ค์ • ํŒŒ์ผ ์šฐ์„  ๊ฒ€์ƒ‰ if (template !== 'default' && templateToPrefix[template]) { const prefix = templateToPrefix[template]; let templateSpecificFile; if (file.includes('.config.')) { // jest.config.cjs -> drizzle.jest.config.cjs templateSpecificFile = `${prefix}.${file}`; } else if (file.startsWith('.')) { // .prettierrc -> drizzle.prettierrc templateSpecificFile = `${prefix}${file}`; } else { // ์ผ๋ฐ˜ ํŒŒ์ผ์ด๋‚˜ ๋””๋ ‰ํ† ๋ฆฌ์ธ ๊ฒฝ์šฐ templateSpecificFile = `${prefix}.${file}`; } const templateSpecificSrc = path.join( CONFIG.paths.devtools, categoryPath, devtool.value, templateSpecificFile, ); if (await fs.pathExists(templateSpecificSrc)) { src = templateSpecificSrc; console.log(chalk.cyan(` โœ“ Using template-specific config: ${templateSpecificFile}`)); } } if (await fs.pathExists(src)) { await fs.copy(src, dst, { overwrite: true }); console.log(chalk.gray(` โ€— ${file} copied.`)); } else { console.log(chalk.yellow(` โš ๏ธ ${file} not found at ${src}`)); } } } // ํŒจํ‚ค์ง€ ์„ค์น˜ (์„ฑ๋Šฅ ์ตœ์ ํ™”๋œ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‚ฌ์šฉ) async function installPackages(pkgs, pkgManager, dev = true, destDir = process.cwd()) { if (!pkgs || pkgs.length === 0) return; const batch = new PackageBatch(); batch.addMany(pkgs); const resolved = await batch.resolve(); const installCmd = pkgManager === 'npm' ? ['install', dev ? '--save-dev' : '', ...resolved].filter(Boolean) : pkgManager === 'yarn' ? ['add', dev ? '--dev' : '', ...resolved].filter(Boolean) : ['add', dev ? '-D' : '', ...resolved].filter(Boolean); await execa(pkgManager, installCmd, { cwd: destDir, stdio: 'inherit' }); } // package.json ์ˆ˜์ • (์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€ ๋“ฑ) async function updatePackageJson(scripts, destDir, projectName = null) { const pkgPath = path.join(destDir, 'package.json'); const file = editJsonFile(pkgPath, { autosave: true }); // ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์ด ์ œ๊ณต๋œ ๊ฒฝ์šฐ package.json์˜ name ํ•„๋“œ ์—…๋ฐ์ดํŠธ if (projectName) { file.set('name', projectName); console.log(chalk.gray(` โŽฏ package.json name updated to: ${projectName}`)); } Object.entries(scripts).forEach(([k, v]) => file.set(`scripts.${k}`, v)); if (!file.get('scripts.prepare') && fs.existsSync(path.join(destDir, '.huskyrc'))) { file.set('scripts.prepare', 'husky install'); } file.save(); } // docker-compose ์ƒ์„ฑ (ํ˜ธํ™˜์„ฑ ์œ ์ง€์šฉ) async function generateCompose(template, destDir) { try { const composeYml = generateDockerCompose(template); const filePath = path.join(destDir, 'docker-compose.yml'); await fs.writeFile(filePath, composeYml, 'utf8'); const dbType = TEMPLATE_DB[template]; console.log(chalk.gray(` โŽฏ docker-compose.yml generated with ${dbType || 'no database'}`)); return dbType; } catch (error) { console.log(chalk.yellow(`[docker-compose] Warning: ${error.message}`)); return null; } } // Git init & ์ฒซ ์ปค๋ฐ‹ async function gitInitAndFirstCommit(destDir) { const doGit = await confirm({ message: 'Initialize git and make first commit?', initial: true }); if (isCancel(doGit)) return; if (!doGit) return; try { await execa('git', ['init'], { cwd: destDir }); await execa('git', ['add', '.'], { cwd: destDir }); await execa('git', ['commit', '-m', 'init'], { cwd: destDir }); console.log(chalk.green(' โœ“ git initialized and first commit made!')); } catch (e) { printError( new CLIError('git init/commit failed', 'git', 'Check git is installed and accessible.'), ); } } // ========== [๋ฉ”์ธ CLI ์‹คํ–‰ ํ๋ฆ„] ========== async function main() { // 1. Node ๋ฒ„์ „ ์ฒดํฌ validateNodeVersion(CONFIG.minNodeVersion); // 2. ํ…œํ”Œ๋ฆฟ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ try { const templateResults = validateAllTemplates(); const violations = Object.entries(templateResults).filter(([_, result]) => !result.valid); if (violations.length > 0) { const violationList = violations .map(([name, result]) => `${name}: ${result.violations.join(', ')}`) .join('\n'); throw new CLIError( `Template integrity violations found:\n${violationList}`, 'template-integrity', 'Run "node bin/validate-templates.js" to fix these issues', ); } } catch (error) { if (error instanceof CLIError) { throw error; } // Other validation errors are non-critical, just log them console.warn(chalk.yellow('โš ๏ธ Template validation warning:'), error.message); } const config = getEnvironmentConfig(); if (!config.env.skipVersionCheck) { await checkForUpdate(); } intro(config.banner.gradient); // 3. ์„ค์ • ๋ชจ๋“œ ์„ ํƒ const setupMode = await select({ message: 'Choose setup mode:', options: [ { label: '๐Ÿš€ Quick start (recommended preset)', value: 'quick', hint: 'Automatically selects a sensible toolset', }, { label: '๐Ÿ›  Custom (step-by-step)', value: 'custom', hint: 'Pick tools category by category', }, ], initialValue: 'quick', }); if (isCancel(setupMode)) return cancel('โŒ Aborted.'); // 3. ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ € ์„ ํƒ + ๊ธ€๋กœ๋ฒŒ ์„ค์น˜ ํ™•์ธ let pkgManager; while (true) { pkgManager = await select({ message: 'Which package manager do you want to use?', options: PACKAGE_MANAGER, initialValue: 'npm', }); if (isCancel(pkgManager)) return cancel('โŒ Aborted.'); if (await checkPkgManagerInstalled(pkgManager)) break; printError( new CLIError( `${pkgManager} is not installed globally!`, 'package-manager', `Install ${pkgManager} first, then run the command again.`, ), ); } note(`Using: ${pkgManager}`); // 4. ํ…œํ”Œ๋ฆฟ ์„ ํƒ const templateDirs = (await fs.readdir(CONFIG.paths.templates)).filter((f) => fs.statSync(path.join(CONFIG.paths.templates, f)).isDirectory(), ); if (templateDirs.length === 0) return printError(new CLIError('No templates found!', 'template-selection')); const options = TEMPLATES_VALUES.filter((t) => t.active && templateDirs.includes(t.value)).map( (t) => ({ label: t.name, // UI์— ํ‘œ์‹œ๋  ์ด๋ฆ„ (โœ… ํฌํ•จ) value: t.value, // ์„ ํƒ ๊ฐ’ hint: `${t.desc} ยท ${t.complexity} ยท ${t.maturity}`, }), ); const template = await select({ message: 'Choose a template:', options: options, initialValue: 'default', }); if (isCancel(template)) return cancel('โŒ Aborted.'); // 5. ํ”„๋กœ์ ํŠธ๋ช… ์ž…๋ ฅ (์ค‘๋ณต์ฒดํฌ/๋ฎ์–ด์“ฐ๊ธฐ) let projectName, destDir; let shouldOverwrite = false; while (true) { const rawProjectName = await text({ message: 'Enter your project name:', initial: CONFIG.defaultProjectName, validate: (val) => { try { validateProjectName(val); return undefined; } catch (error) { return error.message; } }, }); if (isCancel(rawProjectName)) return cancel('โŒ Aborted.'); projectName = sanitizeInput(rawProjectName); destDir = validateProjectPath(path.resolve(process.cwd(), projectName)); if (await fs.pathExists(destDir)) { const overwrite = await confirm({ message: `Directory "${projectName}" already exists. Overwrite?`, initial: false, }); if (isCancel(overwrite)) return cancel('โŒ Aborted.'); if (overwrite) { shouldOverwrite = true; break; } } else break; } // 6. ๊ฐœ๋ฐœ ๋„๊ตฌ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์„ ํƒ let devtoolValues = []; const groupedDevtools = getGroupedDevtools(); if (setupMode === 'quick') { const setupProfile = await select({ message: 'Choose quick profile:', options: [ { label: 'Minimal', value: 'minimal', hint: 'Template only (no additional devtools)', }, { label: 'Recommended', value: 'recommended', hint: 'Balanced default toolchain', }, { label: 'Full', value: 'full', hint: 'One tool from each category', }, ], initialValue: 'recommended', }); if (isCancel(setupProfile)) return cancel('โŒ Aborted.'); devtoolValues = getPresetDevtools(setupProfile, groupedDevtools); note( devtoolValues.length > 0 ? `Quick profile selected: ${setupProfile} (${devtoolValues.join(', ')})` : `Quick profile selected: ${setupProfile} (no additional devtools)`, 'Preset', ); } else { for (const [category, tools] of Object.entries(groupedDevtools)) { const picked = await select({ message: `Select a tool for "${category}":`, options: [ { label: 'Skip', value: null, hint: 'Do not add a tool for this category' }, ...tools.map(({ name, value, desc }) => ({ label: `${name}`, value, hint: `${desc} ${getBenchmarkInfo({ value }, template)}`, })), ], initialValue: null, }); if (isCancel(picked)) return cancel('โŒ Aborted.'); if (picked) devtoolValues.push(picked); } } devtoolValues = resolveDependencies(devtoolValues); note( buildSetupSummary({ pkgManager, template, projectName, devtoolValues, setupMode, }), 'Configuration summary', ); const proceed = await confirm({ message: 'Proceed with project generation?', initial: true, }); if (isCancel(proceed) || !proceed) return cancel('โŒ Aborted.'); // === [์ง„ํ–‰] === // [1] ํ…œํ”Œ๋ฆฟ ๋ณต์‚ฌ const spinner = ora('Copying template...').start(); try { if (shouldOverwrite) { await fs.emptyDir(destDir); } await fs.copy(path.join(CONFIG.paths.templates, template), destDir, { overwrite: true }); spinner.succeed('Template copied!'); } catch (e) { spinner.fail('Template copy failed!'); printError(new FileSystemError(e.message, destDir, 'Check templates folder and permissions.')); return process.exit(1); } // [1-0] package.json name ํ•„๋“œ ์—…๋ฐ์ดํŠธ await updatePackageJson({}, destDir, projectName); // [1-1] Testing ๋„๊ตฌ๋ฅผ ์„ ํƒํ•œ ๊ฒฝ์šฐ์—๋งŒ /src/test ์˜ˆ์ œ ๋ณต์‚ฌ // ์ฃผ์„: ์ด์ œ files ๋ฐฐ์—ด์— 'src/test'๊ฐ€ ํฌํ•จ๋˜์–ด copyDevtoolFiles์—์„œ ์ž๋™ ์ฒ˜๋ฆฌ๋จ /* const testDevtool = devtoolValues .map((val) => DEVTOOLS_VALUES.find((d) => d.value === val)) .find((tool) => tool && tool.category === 'Testing'); if (testDevtool) { const devtoolTestDir = path.join(CONFIG.paths.devtools, testDevtool.value, 'src', 'test'); const projectTestDir = path.join(destDir, 'src', 'test'); if (await fs.pathExists(devtoolTestDir)) { await fs.copy(devtoolTestDir, projectTestDir, { overwrite: true }); console.log(chalk.gray(` โŽฏ test files for ${testDevtool.name} copied.`)); } } */ // [2] ๊ฐœ๋ฐœ ๋„๊ตฌ ํŒŒ์ผ/ํŒจํ‚ค์ง€/์Šคํฌ๋ฆฝํŠธ/์ฝ”๋“œํŒจ์น˜ for (const [index, val] of devtoolValues.entries()) { const tool = DEVTOOLS_VALUES.find((d) => d.value === val); if (!tool) continue; spinner.start(`Setting up ${tool.name} (${index + 1}/${devtoolValues.length})...`); await copyDevtoolFiles(tool, destDir, template); // [2-1] ๊ฐœ๋ฐœ ๋„๊ตฌ - ํŒจํ‚ค์ง€ ์„ค์น˜ if (tool.pkgs?.length > 0) await installPackages(tool.pkgs, pkgManager, false, destDir); if (tool.devPkgs?.length > 0) await installPackages(tool.devPkgs, pkgManager, true, destDir); // [2-2] ๊ฐœ๋ฐœ ๋„๊ตฌ - ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€ ๋“ฑ if (Object.keys(tool.scripts).length) await updatePackageJson(tool.scripts, destDir); // [2-2-1] ๊ฐœ๋ฐœ ๋„๊ตฌ - postInstall ํ•จ์ˆ˜ ์‹คํ–‰ (Jest ํƒ€์ž… ์„ค์ • ๋“ฑ) if (tool.postInstall && typeof tool.postInstall === 'function') { try { tool.postInstall(destDir); console.log(chalk.gray(` โŽฏ ${tool.name} postInstall completed.`)); } catch (error) { console.log(chalk.yellow(` โš ๏ธ ${tool.name} postInstall warning:`, error.message)); } } // [2-3] ๊ฐœ๋ฐœ ๋„๊ตฌ - Docker ์„ ํƒ ํ•œ ๊ฒฝ์šฐ, ํŒŒ์ผ ๊ธฐ๋ฐ˜ Docker ์„ค์ • ์ƒ์„ฑ if (tool.value === 'docker') { try { await generateDockerFiles(template, destDir); console.log(chalk.gray(` โŽฏ Docker environment configured for ${template}`)); } catch (error) { console.log(chalk.yellow(` โš ๏ธ Docker setup warning: ${error.message}`)); // Fallback to legacy method await generateCompose(template, destDir); } } // [2-4] ๊ฐœ๋ฐœ ๋„๊ตฌ - Swagger ์„ ํƒ ์‹œ์—๋งŒ app.ts AST ํŒจ์น˜ if (tool.value === 'swagger') { await injectSwaggerIntoApp(destDir); } spinner.succeed(`${tool.name} setup done.`); } // [3] ํ…œํ”Œ๋ฆฟ ๊ธฐ๋ณธ ํŒจํ‚ค์ง€ ์„ค์น˜ spinner.start(`Installing base dependencies with ${pkgManager}...`); await execa(pkgManager, ['install'], { cwd: destDir, stdio: 'inherit' }); spinner.succeed('๐Ÿ“ฆ Base dependencies installed!'); // [4] git ์ฒซ ์ปค๋ฐ‹ ์˜ต์…˜ // await gitInitAndFirstCommit(destDir); outro(chalk.greenBright('\n๐ŸŽ‰ Project setup complete!\n')); console.log(chalk.cyan(` $ cd ${projectName}`)); console.log(chalk.cyan(` $ ${pkgManager} run dev\n`)); // Docker๋ฅผ ์„ ํƒํ•œ ๊ฒฝ์šฐ ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด const hasDocker = devtoolValues.includes('docker'); if (hasDocker) { const dbType = TEMPLATE_DB[template]; console.log(chalk.yellow('๐Ÿณ Docker setup:')); console.log(chalk.gray(` $ ${pkgManager} run docker:dev # Start development environment`)); if (dbType) { console.log( chalk.gray(` $ ${pkgManager} run db:migrate # Run migrations (if applicable)`), ); console.log( chalk.gray(` $ ${pkgManager} run db:seed # Insert sample data (if applicable)`), ); } console.log(chalk.gray(` $ ${pkgManager} run docker:down # Stop containers\n`)); } console.log(chalk.gray('โœจ Happy hacking!\n')); } main().catch((err) => { if (err instanceof CLIError) { printError(err); } else { printError(new CLIError('Unexpected error', null, err.message)); } process.exit(err.code || 1); });