starkon
Version:
Complete Next.js boilerplate with authentication, i18n & CLI - Create production-ready apps instantly
1,787 lines (1,578 loc) • 56.4 kB
JavaScript
#!/usr/bin/env node
import fs from 'fs-extra'
import path from 'path'
import os from 'os'
import { fileURLToPath } from 'url'
import { program } from 'commander'
import prompts from 'prompts'
import ora from 'ora'
// Centralized high-performance logging system
class StarkonLogger {
constructor() {
this.isDevelopment = process.env.NODE_ENV !== 'production'
this.logLevel = process.env.LOG_LEVEL || (this.isDevelopment ? 'debug' : 'info')
this.levels = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }
this.currentLevel = this.levels[this.logLevel] || 1
this.correlationId = null
// Performance: Pre-bind methods to avoid function creation overhead
this.debug = this._createLogMethod('debug').bind(this)
this.info = this._createLogMethod('info').bind(this)
this.warn = this._createLogMethod('warn').bind(this)
this.error = this._createLogMethod('error').bind(this)
this.fatal = this._createLogMethod('fatal').bind(this)
}
setCorrelationId(id) {
this.correlationId = id || Math.random().toString(36).substring(7)
return this.correlationId
}
_shouldLog(level) {
return this.levels[level] >= this.currentLevel
}
_formatMessage(level, module, message, context = {}) {
const timestamp = new Date().toISOString()
if (this.isDevelopment) {
// Pretty format for development
const prefix = this._getColoredPrefix(level)
const moduleStr = module ? `[${module}]` : ''
const contextStr = Object.keys(context).length > 0 ? ` ${JSON.stringify(context)}` : ''
return `${prefix} ${timestamp.substring(11, 23)} ${moduleStr} ${message}${contextStr}`
} else {
// Structured JSON for production
return JSON.stringify({
level,
time: timestamp,
module,
correlationId: this.correlationId,
message,
...context,
})
}
}
_getColoredPrefix(level) {
// Avoid chalk dependency for performance, use ANSI codes directly
const colors = {
debug: '\x1b[36m[DEBUG]\x1b[0m', // Cyan
info: '\x1b[32m[INFO]\x1b[0m', // Green
warn: '\x1b[33m[WARN]\x1b[0m', // Yellow
error: '\x1b[31m[ERROR]\x1b[0m', // Red
fatal: '\x1b[35m[FATAL]\x1b[0m', // Magenta
}
return colors[level] || '[LOG]'
}
_createLogMethod(level) {
return (messageOrContext, message, context = {}) => {
if (!this._shouldLog(level)) return
let finalMessage, finalContext, moduleName
if (typeof messageOrContext === 'string') {
finalMessage = messageOrContext
finalContext = typeof message === 'object' ? message : context
moduleName = typeof message === 'string' ? message : undefined
} else if (typeof messageOrContext === 'object' && messageOrContext !== null) {
finalMessage = message || 'Log message'
finalContext = messageOrContext
moduleName = messageOrContext.module
} else {
finalMessage = String(messageOrContext)
finalContext = context
}
const formatted = this._formatMessage(level, moduleName, finalMessage, finalContext)
// Use appropriate console method
const consoleMethod =
level === 'error' || level === 'fatal' ? console.error : level === 'warn' ? console.warn : console.log
consoleMethod(formatted)
}
}
// Create module-specific logger
createModuleLogger(moduleName) {
return {
debug: (msg, ctx) => this.debug({ module: moduleName, ...ctx }, msg),
info: (msg, ctx) => this.info({ module: moduleName, ...ctx }, msg),
warn: (msg, ctx) => this.warn({ module: moduleName, ...ctx }, msg),
error: (msg, ctx) => this.error({ module: moduleName, ...ctx }, msg),
fatal: (msg, ctx) => this.fatal({ module: moduleName, ...ctx }, msg),
}
}
// Legacy console compatibility for gradual migration
overrideConsole() {
const originalConsole = { ...console }
console.log = (...args) => {
if (args.length === 1 && typeof args[0] === 'string') {
this.info(args[0])
} else {
this.info({ data: args }, 'Console log')
}
}
console.error = (...args) => {
if (args[0] instanceof Error) {
this.error({ error: args[0].message, stack: args[0].stack }, args[0].message)
} else {
this.error({ data: args }, 'Console error')
}
}
console.warn = (...args) => {
this.warn({ data: args }, 'Console warning')
}
return originalConsole
}
}
// Global logger instance
const logger = new StarkonLogger()
logger.setCorrelationId() // Set initial correlation ID
// ES modules için __dirname alternatifi
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Thread-safe global state management
class StateManager {
constructor() {
this._configCache = null
this._localesCache = new Map()
this._configLock = false
this._localeLocks = new Set()
}
async getConfig() {
// Simple mutex-like locking
while (this._configLock) {
await new Promise((resolve) => setTimeout(resolve, 10))
}
return this._configCache
}
async setConfig(config) {
this._configLock = true
try {
this._configCache = config
} finally {
this._configLock = false
}
}
async getLocale(key) {
// Avoid concurrent access to same locale
while (this._localeLocks.has(key)) {
await new Promise((resolve) => setTimeout(resolve, 10))
}
return this._localesCache.get(key)
}
async setLocale(key, value) {
this._localeLocks.add(key)
try {
this._localesCache.set(key, value)
} finally {
this._localeLocks.delete(key)
}
}
hasLocale(key) {
return this._localesCache.has(key)
}
clearAll() {
this._configCache = null
this._localesCache.clear()
this._localeLocks.clear()
}
}
const stateManager = new StateManager()
const shutdownLog = logger.createModuleLogger('shutdown')
// Graceful shutdown manager
class ShutdownManager {
constructor() {
this.isShuttingDown = false
this.cleanupTasks = []
this.exitTimeout = null
// Add state manager cleanup task
this.addCleanupTask(async () => {
stateManager.clearAll()
}, 'State Manager cleanup')
// Signal handlers
process.on('SIGTERM', this.gracefulShutdown.bind(this))
process.on('SIGINT', this.gracefulShutdown.bind(this))
process.on('uncaughtException', this.handleCriticalError.bind(this))
process.on('unhandledRejection', this.handleCriticalError.bind(this))
}
addCleanupTask(task, description = 'Unknown task') {
this.cleanupTasks.push({ task, description })
}
async gracefulShutdown(signal) {
if (this.isShuttingDown) return
this.isShuttingDown = true
shutdownLog.warn(`Graceful shutdown initiated by ${signal} signal`, { signal })
// 10 saniye timeout - zorunlu çıkış
this.exitTimeout = setTimeout(() => {
shutdownLog.error('Shutdown timeout - forcing exit', { timeoutMs: 5000 })
process.exit(1)
}, 10000)
try {
// Cleanup tasks'leri çalıştır
for (const { task, description } of this.cleanupTasks) {
try {
shutdownLog.info(`Cleaning up: ${description}`, { task: description })
await task()
} catch (error) {
shutdownLog.error(`Cleanup error: ${description}`, {
task: description,
error: error.message,
stack: error.stack,
})
}
}
shutdownLog.info('Graceful shutdown completed successfully')
clearTimeout(this.exitTimeout)
process.exit(0)
} catch (error) {
shutdownLog.fatal('Critical error during shutdown', { error: error.message, stack: error.stack })
clearTimeout(this.exitTimeout)
process.exit(1)
}
}
handleCriticalError(error) {
shutdownLog.fatal('Critical error caught', { error: error.message, stack: error.stack })
if (!this.isShuttingDown) {
this.gracefulShutdown('CRITICAL_ERROR')
}
}
safeExit(code = 0) {
if (this.isShuttingDown) {
return // Zaten shutdown sürecinde
}
// Normal exit için cleanup tasks varsa çalıştır
if (this.cleanupTasks.length > 0) {
this.gracefulShutdown('MANUAL_EXIT')
.then(() => {
process.exit(code)
})
.catch(() => {
process.exit(1)
})
} else {
process.exit(code)
}
}
}
const shutdownManager = new ShutdownManager()
const importLog = logger.createModuleLogger('imports')
const configLog = logger.createModuleLogger('config')
const localeLog = logger.createModuleLogger('locale')
const sysLog = logger.createModuleLogger('system')
const pluginLog = logger.createModuleLogger('plugins')
const templateLog = logger.createModuleLogger('templates')
const fileLog = logger.createModuleLogger('files')
const createLog = logger.createModuleLogger('create')
const cliLog = logger.createModuleLogger('cli')
const helpLog = logger.createModuleLogger('help')
const updateLog = logger.createModuleLogger('updates')
/**
* Özel hata sınıfı - Hoisting sorunu için yukarı taşındı
*/
class StarkonError extends Error {
constructor(message, code, details = null) {
super(message)
this.name = 'StarkonError'
this.code = code
this.details = details
}
}
// Fetch API compatibility check ve polyfill
let fetchPolyfill
try {
// Node.js 18+ fetch kontrolü
if (typeof globalThis.fetch === 'undefined') {
// Fetch mevcut değilse polyfill yükle
fetchPolyfill = await import('https')
.then(() => {
// HTTPS modülü mevcut, basit HTTP client oluştur
return async (url, options = {}) => {
const https = await import('https')
const urlParsed = new URL(url)
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: urlParsed.hostname,
port: urlParsed.port || 443,
path: urlParsed.pathname + urlParsed.search,
method: options.method || 'GET',
headers: {
'User-Agent': 'starkon/0.0.12',
...options.headers,
},
},
(res) => {
let data = ''
res.on('data', (chunk) => (data += chunk))
res.on('end', () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
json: () => Promise.resolve(JSON.parse(data)),
text: () => Promise.resolve(data),
})
})
},
)
req.on('error', reject)
req.setTimeout(10000, () => reject(new Error('Timeout')))
if (options.body) {
req.write(options.body)
}
req.end()
})
}
})
.catch(() => null)
}
} catch {
fetchPolyfill = null
}
// Fetch function - native veya polyfill
const safeFetch =
globalThis.fetch ||
fetchPolyfill ||
(() => {
throw new Error('Fetch API mevcut değil ve polyfill yüklenemedi')
})
// Enhanced dynamic import with retry and error handling
async function safeImport(moduleName, fallback = null) {
const maxRetries = 2
let lastError
for (let i = 0; i < maxRetries; i++) {
try {
const importedModule = await import(moduleName)
return importedModule.default || importedModule
} catch (error) {
lastError = error
// Network error'larda kısa retry
if (error.code === 'ERR_NETWORK' && i < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, 100 * (i + 1)))
continue
}
break
}
}
// Fallback kullan veya error fırlat
if (fallback) {
importLog.warn(`Module loading failed, using fallback`, { module: moduleName, attempts: maxRetries })
return fallback
} else {
throw new Error(`Module yüklenemedi: ${moduleName} - ${lastError.message}`)
}
}
// Chalk'ı güvenli dynamic import ile yükle
let chalk
try {
chalk = await safeImport('chalk', {
// Enhanced fallback with more colors and styles
red: (text) => `\x1b[31m${text}\x1b[0m`,
green: (text) => `\x1b[32m${text}\x1b[0m`,
blue: (text) => `\x1b[34m${text}\x1b[0m`,
yellow: (text) => `\x1b[33m${text}\x1b[0m`,
cyan: (text) => `\x1b[36m${text}\x1b[0m`,
magenta: (text) => `\x1b[35m${text}\x1b[0m`,
gray: (text) => `\x1b[90m${text}\x1b[0m`,
white: (text) => `\x1b[37m${text}\x1b[0m`,
bold: {
blue: (text) => `\x1b[1m\x1b[34m${text}\x1b[0m`,
green: (text) => `\x1b[1m\x1b[32m${text}\x1b[0m`,
red: (text) => `\x1b[1m\x1b[31m${text}\x1b[0m`,
yellow: (text) => `\x1b[1m\x1b[33m${text}\x1b[0m`,
},
dim: (text) => `\x1b[2m${text}\x1b[0m`,
underline: (text) => `\x1b[4m${text}\x1b[0m`,
})
} catch (error) {
importLog.fatal('Critical error loading chalk module', { error: error.message, stack: error.stack })
shutdownManager.safeExit(1)
}
/**
* Configuration Management System
*/
async function loadUserConfig() {
const cachedConfig = await stateManager.getConfig()
if (cachedConfig) return cachedConfig
try {
const configDir = path.join(os.homedir(), '.starkon')
const configPath = path.join(configDir, 'config.json')
let config
if (await fs.pathExists(configPath)) {
const userConfig = await fs.readJson(configPath)
config = { ...getDefaultConfig(), ...userConfig }
} else {
config = getDefaultConfig()
}
await stateManager.setConfig(config)
return config
} catch (error) {
configLog.warn('Config loading error', {
error: error.message,
configPath: path.join(os.homedir(), '.starkon', 'config.json'),
})
const defaultConfig = getDefaultConfig()
await stateManager.setConfig(defaultConfig)
return defaultConfig
}
}
function getDefaultConfig() {
return {
defaultTemplate: null,
preferredPackageManager: 'auto',
skipGit: false,
skipUpdateCheck: false,
telemetryEnabled: true,
locale: 'tr',
themes: {
colors: true,
spinners: true,
},
}
}
async function saveUserConfig(updates) {
try {
const configDir = path.join(os.homedir(), '.starkon')
const configPath = path.join(configDir, 'config.json')
await fs.ensureDir(configDir)
const currentConfig = await loadUserConfig()
const newConfig = { ...currentConfig, ...updates }
await fs.writeJson(configPath, newConfig, { spaces: 2 })
await stateManager.setConfig(newConfig)
return true
} catch (error) {
configLog.error('Config save error', {
error: error.message,
updates,
configPath: path.join(os.homedir(), '.starkon', 'config.json'),
})
return false
}
}
/**
* Internationalization System
*/
async function loadLocale(locale = 'tr') {
if (stateManager.hasLocale(locale)) {
return await stateManager.getLocale(locale)
}
try {
const localeMessages = await getLocaleMessages(locale)
await stateManager.setLocale(locale, localeMessages)
return localeMessages
} catch (error) {
localeLog.warn('Locale loading error', {
locale,
error: error.message,
localesDir: path.join(__dirname, 'locales'),
})
// Fallback to Turkish
if (locale !== 'tr') {
try {
const fallbackMessages = await getLocaleMessages('tr')
await stateManager.setLocale(locale, fallbackMessages)
return fallbackMessages
} catch {
// En son fallback - hardcoded mesajlar
const hardcodedMessages = {
PROJECT_CREATING: '🌊 Yeni proje oluşturuluyor...',
OPERATION_CANCELLED: 'İşlem iptal edildi.',
ERROR_OCCURRED: 'hata oluştu',
}
await stateManager.setLocale(locale, hardcodedMessages)
return hardcodedMessages
}
}
throw error
}
}
async function getLocaleMessages(locale) {
const messages = {
tr: {
PROJECT_CREATING: '🌊 Starkon boilerplate ile yeni Next.js projesi oluşturuluyor...',
PROJECT_CREATED_SUCCESS: '🎉 Starkon boilerplate projesi başarıyla oluşturuldu!',
PROJECT_NAME_PROMPT: 'Proje adını girin:',
PROJECT_NAME_EMPTY: 'Proje adı boş olamaz',
PROJECT_NAME_TOO_LONG: 'Proje adı 50 karakterden uzun olamaz',
PROJECT_NAME_INVALID: 'Proje adı sadece harf, rakam, nokta, tire ve alt çizgi içerebilir',
TEMPLATE_SELECTION: '📋 Mevcut Template Seçenekleri:',
TEMPLATE_SELECTED: 'seçildi',
FEATURES: 'Özellikler:',
OPERATION_CANCELLED: 'İşlem iptal edildi.',
DIRECTORY_EXISTS: 'dizini zaten var ve boş değil. Devam edilsin mi?',
ANALYZING_TEMPLATES: 'Template dosyaları analiz ediliyor...',
CREATING_STRUCTURE: 'Proje yapısı oluşturuluyor...',
COPYING_FILES: 'Template dosyaları kopyalanıyor...',
CUSTOMIZING_PACKAGE: 'Package.json özelleştiriliyor...',
INITIALIZING_GIT: 'Git repository hazırlanıyor...',
FINAL_CHECKS: 'Son kontroller yapılıyor...',
GET_STARTED: 'Başlamak için aşağıdaki komutları çalıştırın:',
ALTERNATIVE_PM: 'Alternatif olarak',
MORE_INFO: 'Daha fazla bilgi için:',
HAPPY_CODING: 'Keyifli kodlamalar!',
ERROR_OCCURRED: 'hata oluştu',
CLEANUP_SUCCESS: 'Yarım kalan dosyalar temizlendi.',
UPDATE_AVAILABLE: '🆕 Yeni versiyon mevcut!',
UPDATE_COMMAND: 'Güncellemek için: npm install -g starkon@latest',
},
en: {
PROJECT_CREATING: '🌊 Creating new Next.js project with Starkon boilerplate...',
PROJECT_CREATED_SUCCESS: '🎉 Starkon boilerplate project created successfully!',
PROJECT_NAME_PROMPT: 'Enter project name:',
PROJECT_NAME_EMPTY: 'Project name cannot be empty',
PROJECT_NAME_TOO_LONG: 'Project name cannot be longer than 50 characters',
PROJECT_NAME_INVALID: 'Project name can only contain letters, numbers, dots, hyphens and underscores',
TEMPLATE_SELECTION: '📋 Available Template Options:',
TEMPLATE_SELECTED: 'selected',
FEATURES: 'Features:',
OPERATION_CANCELLED: 'Operation cancelled.',
DIRECTORY_EXISTS: 'directory already exists and is not empty. Continue?',
ANALYZING_TEMPLATES: 'Analyzing template files...',
CREATING_STRUCTURE: 'Creating project structure...',
COPYING_FILES: 'Copying template files...',
CUSTOMIZING_PACKAGE: 'Customizing package.json...',
INITIALIZING_GIT: 'Initializing git repository...',
FINAL_CHECKS: 'Running final checks...',
GET_STARTED: 'To get started, run the following commands:',
ALTERNATIVE_PM: 'Alternatively you can use',
MORE_INFO: 'For more information:',
HAPPY_CODING: 'Happy coding!',
ERROR_OCCURRED: 'error occurred',
CLEANUP_SUCCESS: 'Cleaned up incomplete files.',
UPDATE_AVAILABLE: '🆕 New version available!',
UPDATE_COMMAND: 'To update: npm install -g starkon@latest',
},
}
return messages[locale] || messages.tr
}
/**
* System Requirements Validation
*/
async function validateSystemRequirements() {
const requirements = {
node: '>=18.0.0',
npm: '>=8.0.0',
}
// Node.js version check
const nodeVersion = process.version
if (!satisfiesVersion(nodeVersion, requirements.node)) {
throw new StarkonError(`Node.js ${requirements.node} gerekiyor, mevcut: ${nodeVersion}`, 'UNSUPPORTED_NODE_VERSION')
}
// NPM version check (optional)
try {
const childProcess = await safeImport('child_process')
const npmVersion = childProcess.execSync('npm --version', { encoding: 'utf8' }).trim()
if (!satisfiesVersion(`v${npmVersion}`, requirements.npm.replace('>=', '>='))) {
sysLog.warn('NPM version recommendation', { recommended: requirements.npm, current: `v${npmVersion}` })
}
} catch {
// NPM version check optional - silent fail
}
return true
}
function satisfiesVersion(current, required) {
// Input validation ve güvenlik kontrolleri
if (!current || !required || typeof current !== 'string' || typeof required !== 'string') {
return false
}
// String uzunluğu limiti (ReDoS koruması)
if (current.length > 50 || required.length > 50) {
return false
}
// Güvenli version parsing - sadece numeric kısımları alır
const parseVersion = (v) => {
const cleaned = v.replace(/^v/, '').trim()
// Sadece valid semver karakterleri: rakamlar, noktalar, tire, artı
if (!/^[\d.\-+a-zA-Z]+$/.test(cleaned)) {
return []
}
const parts = cleaned.split('.').slice(0, 3) // En fazla major.minor.patch
return parts.map((part) => {
// Pre-release ve build metadata'yı temizle
const numericPart = part.split(/[-+]/)[0]
const num = parseInt(numericPart, 10)
return isNaN(num) ? 0 : num
})
}
// Operator parsing - güvenli regex ile
let reqOp = '>='
let reqVer = required
const operatorMatch = required.match(/^(>=|<=|>|<|=)(.+)$/)
if (operatorMatch) {
reqOp = operatorMatch[1]
reqVer = operatorMatch[2].trim()
} else {
// Fallback: eğer operator yoksa >= varsay
reqVer = required.replace(/^>=\s*/, '')
}
const currentParts = parseVersion(current)
const requiredParts = parseVersion(reqVer)
// Version array'leri boşsa geçersiz
if (currentParts.length === 0 || requiredParts.length === 0) {
return false
}
// Normalize to same length (3 parts: major.minor.patch)
while (currentParts.length < 3) currentParts.push(0)
while (requiredParts.length < 3) requiredParts.push(0)
// Version karşılaştırması
for (let i = 0; i < 3; i++) {
const curr = currentParts[i]
const req = requiredParts[i]
if (curr > req) {
return reqOp === '>' || reqOp === '>=' || reqOp === '!='
}
if (curr < req) {
return reqOp === '<' || reqOp === '<=' || reqOp === '!='
}
}
// Versions are equal
return reqOp === '=' || reqOp === '>=' || reqOp === '<='
}
/**
* Template Caching System
*/
async function getCacheDir() {
const cacheDir = path.join(os.tmpdir(), 'starkon-cache')
await fs.ensureDir(cacheDir)
return cacheDir
}
async function cacheTemplate(templateKey, templateData) {
try {
const cacheDir = await getCacheDir()
const cachePath = path.join(cacheDir, `${templateKey}.json`)
const cacheData = {
timestamp: Date.now(),
version: '0.0.12',
data: templateData,
}
await fs.writeJson(cachePath, cacheData)
return true
} catch {
return false
}
}
async function getCachedTemplate(templateKey) {
try {
const cacheDir = await getCacheDir()
const cachePath = path.join(cacheDir, `${templateKey}.json`)
if (await fs.pathExists(cachePath)) {
const cached = await fs.readJson(cachePath)
// Cache valid for 24 hours
if (Date.now() - cached.timestamp < 24 * 60 * 60 * 1000) {
return cached.data
}
}
return null
} catch {
return null
}
}
/**
* Plugin System Architecture
*/
class PluginManager {
constructor() {
this.plugins = []
this.hooks = {
beforeProjectCreate: [],
afterProjectCreate: [],
beforeTemplateSelect: [],
afterTemplateSelect: [],
beforeFilesCopy: [],
afterFilesCopy: [],
}
}
async loadPlugins() {
try {
const config = await loadUserConfig()
const pluginPaths = config.plugins || []
for (const pluginPath of pluginPaths) {
await this.loadPlugin(pluginPath)
}
} catch {
// Plugin loading hatası critical değil
}
}
async loadPlugin(pluginPath) {
try {
const plugin = await safeImport(pluginPath)
if (plugin && typeof plugin === 'object') {
this.plugins.push(plugin)
this.registerHooks(plugin)
}
} catch (error) {
pluginLog.warn('Plugin loading failed', { pluginPath, error: error.message })
}
}
registerHooks(plugin) {
for (const [hookName, hookFn] of Object.entries(plugin)) {
if (this.hooks[hookName] && typeof hookFn === 'function') {
this.hooks[hookName].push(hookFn)
}
}
}
async executeHook(hookName, context) {
const hooks = this.hooks[hookName] || []
for (const hook of hooks) {
try {
await hook(context)
} catch {
// Individual hook hatası tüm process'i durdurmamalı
}
}
}
}
const pluginManager = new PluginManager()
/**
* Package manager'ı detect eden fonksiyon
*/
async function detectPackageManager() {
// Global olarak yüklü package manager'ları kontrol et
try {
const childProcess = await safeImport('child_process')
// pnpm kontrolü
try {
childProcess.execSync('pnpm --version', { stdio: 'ignore' })
return 'pnpm'
} catch {
// pnpm not available
}
// yarn kontrolü
try {
childProcess.execSync('yarn --version', { stdio: 'ignore' })
return 'yarn'
} catch {
// yarn not available
}
// npm her zaman mevcut (Node.js ile gelir)
return 'npm'
} catch {
return 'npm'
}
}
/**
* Package manager'a göre komutları döndüren fonksiyon
*/
function getPackageManagerCommands(packageManager) {
const commands = {
npm: {
install: 'npm install',
dev: 'npm run dev',
build: 'npm run build',
start: 'npm start',
},
yarn: {
install: 'yarn install',
dev: 'yarn dev',
build: 'yarn build',
start: 'yarn start',
},
pnpm: {
install: 'pnpm install',
dev: 'pnpm dev',
build: 'pnpm build',
start: 'pnpm start',
},
}
return commands[packageManager] || commands.npm
}
/**
* Mevcut template'leri tanımlayan obje
*/
const TEMPLATES = {
basic: {
name: 'Temel Template',
description: 'Minimal Next.js template - sadece temel özellikler',
features: ['Next.js 15', 'TypeScript', 'Tailwind CSS', 'ESLint'],
excludeFiles: [
'src/components/ui',
'src/components/forms',
'src/lib/services/mockAuthService.ts',
'src/lib/services/authApiService.ts',
'src/providers/AuthProvider.tsx',
'src/hooks/useAuth.ts',
'src/app/(auth)',
'src/app/login',
'src/app/register',
'src/app/forgot-password',
'src/app/verify-email',
'src/app/reset-password',
'src/locales',
'src/lib/i18n.ts',
'src/providers/I18nProvider.tsx',
],
},
standard: {
name: 'Next.js Boilerplate',
description: 'Tam özellikli boilerplate - authentication, i18n ve tüm componentler dahil',
features: ['Next.js 15', 'TypeScript', 'Tailwind CSS', 'Authentication', 'i18n', 'Comprehensive UI Kit'],
excludeFiles: [], // Hiçbir dosya exclude edilmez
},
dashboard: {
name: 'Dashboard Template',
description: 'Admin dashboard için optimize edilmiş template',
features: ['Next.js 15', 'TypeScript', 'Dashboard Layout', 'Data Tables', 'Charts Ready'],
excludeFiles: [
'src/app/public',
'src/app/about',
'src/app/contact',
'src/app/pricing',
'src/app/support',
'src/components/layout/PublicFooter.tsx',
'src/components/layout/PublicNavbar.tsx',
],
},
minimal: {
name: 'Minimal Template',
description: 'En sade template - sadece Next.js ve TypeScript',
features: ['Next.js 15', 'TypeScript', 'Minimal Setup'],
excludeFiles: [
'src/components/ui',
'src/components/forms',
'src/components/layout',
'src/lib/services',
'src/providers',
'src/hooks',
'src/app/(auth)',
'src/app/public',
'src/app/login',
'src/app/register',
'src/app/forgot-password',
'src/app/verify-email',
'src/app/reset-password',
'src/app/about',
'src/app/contact',
'src/app/pricing',
'src/app/support',
'src/locales',
'src/lib/i18n.ts',
'src/store',
],
},
landing: {
name: 'Landing Page Template',
description: 'Tek sayfalık tanıtım sitesi - Hero, Features, Testimonials',
features: ['Next.js 15', 'TypeScript', 'Landing Components', 'Animations', 'Contact Forms'],
excludeFiles: [
'src/app/(auth)',
'src/app/(authentication)',
'src/app/(corporate)',
'src/app/corporate',
'src/app/public/about',
'src/app/public/contact',
'src/app/public/support',
'src/app/page.tsx',
'src/components/auth',
'src/components/corporate',
'src/lib/services/mockAuthService.ts',
'src/lib/services/authApiService.ts',
'src/lib/services/sessionTokenManager.ts',
'src/providers/AuthProvider.tsx',
'src/hooks/useAuth.ts',
'src/locales',
'src/lib/i18n.ts',
'src/providers/I18nProvider.tsx',
'src/components/layout/AuthNavbar.tsx',
'src/components/layout/AuthFooter.tsx',
'src/data/componentDemoData.tsx',
'NPM_PUBLISH.md',
'TEMPLATE_RESEARCH.md',
'TEMPLATE_ROADMAP.md',
],
},
corporate: {
name: 'Corporate Template',
description: 'Kurumsal şirket sitesi - Hakkımızda, Hizmetler, Blog, Galeri',
features: ['Next.js 15', 'TypeScript', 'Corporate Pages', 'Blog System', 'Content Management'],
excludeFiles: [
'src/app/(auth)',
'src/app/(authentication)',
'src/app/public',
'src/app/page.tsx',
'src/components/sections',
'src/components/ui/register',
'src/components/ui/language',
'src/components/ui/settings',
'src/hooks/useAuth.ts',
'src/hooks/useLocale.ts',
'src/lib/locale-utils.ts',
'src/lib/i18n.ts',
'src/locales',
'src/providers/AuthProvider.tsx',
'src/providers/I18nProvider.tsx',
'src/services/authService.ts',
'src/lib/validations/auth.ts',
'src/lib/types/auth.ts',
'src/utils/authDebug.ts',
'src/middleware.ts',
],
},
}
/**
* Template seçim prompt'unu göster
*/
async function selectTemplate() {
const config = await loadUserConfig()
// Load locale for template selection
await loadLocale(config.locale)
templateLog.info('Showing template selection prompt', {
locale: config.locale,
availableTemplates: ['standard', 'landing', 'corporate'],
})
// Sadece ana 3 template'i göster
const mainTemplates = ['standard', 'landing', 'corporate']
const templateChoices = mainTemplates.map((key) => {
const template = TEMPLATES[key]
return {
title: `${template.name}`,
description: template.description,
value: key,
}
})
const response = await prompts({
type: 'select',
name: 'template',
message: "Hangi template'i kullanmak istersiniz?",
choices: templateChoices,
initial: mainTemplates.indexOf(config.defaultTemplate) >= 0 ? mainTemplates.indexOf(config.defaultTemplate) : 0,
})
if (!response.template) {
templateLog.info('Template selection cancelled by user')
shutdownManager.safeExit(0)
return
}
const selectedTemplate = TEMPLATES[response.template]
templateLog.info('Template selected successfully', {
template: selectedTemplate.name,
key: response.template,
features: selectedTemplate.features,
})
return response.template
}
/**
* Gelişmiş Türkçe karakterleri normalize eden fonksiyon
* Unicode normalization + complete Turkish character mapping
*/
function normalizeProjectName(name) {
if (!name || typeof name !== 'string') {
return name
}
try {
// 1. Unicode canonical decomposition (NFD) - diacritik ayırma
let normalized = name.normalize('NFD')
// 2. Kapsamlı Turkish character mapping - tüm varyantlar dahil
const turkishMap = {
// Turkish karakterler - both normal and Unicode forms
ç: 'c',
Ç: 'C',
ğ: 'g',
Ğ: 'G',
ı: 'i',
İ: 'I', // İ ve ı ayrı karakterler!
ö: 'o',
Ö: 'O',
ş: 's',
Ş: 'S',
ü: 'u',
Ü: 'U',
// Diğer yaygın diacritic karakterler
á: 'a',
Á: 'A',
à: 'a',
À: 'A',
â: 'a',
Â: 'A',
é: 'e',
É: 'E',
è: 'e',
È: 'E',
ê: 'e',
Ê: 'E',
í: 'i',
Í: 'I',
ì: 'i',
Ì: 'I',
î: 'i',
Î: 'I',
ó: 'o',
Ó: 'O',
ò: 'o',
Ò: 'O',
ô: 'o',
Ô: 'O',
ú: 'u',
Ú: 'U',
ù: 'u',
Ù: 'U',
û: 'u',
Û: 'U',
ñ: 'n',
Ñ: 'N',
}
// 3. Character mapping uygula
normalized = normalized.replace(/./g, (char) => turkishMap[char] || char)
// 4. Kalan diacritik işaretleri temizle (Unicode category Mn - Nonspacing_Mark)
normalized = normalized.replace(/\p{Diacritic}/gu, '')
// 5. Son normalizasyon (NFC - Canonical Composition)
normalized = normalized.normalize('NFC')
return normalized
} catch (error) {
console.warn(chalk.yellow(`⚠️ String normalization error: ${error.message}`))
// Fallback - basit character replacement
const basicTurkishMap = {
ç: 'c',
Ç: 'C',
ğ: 'g',
Ğ: 'G',
ı: 'i',
İ: 'I',
ö: 'o',
Ö: 'O',
ş: 's',
Ş: 'S',
ü: 'u',
Ü: 'U',
}
return name.replace(/[çÇğĞıİöÖşŞüÜ]/g, (char) => basicTurkishMap[char] || char)
}
}
/**
* Gelişmiş proje adı validasyon fonksiyonu
*/
function validateProjectName(name) {
// Temel kontroller
const checks = [
{
test: name && name.trim() !== '',
message: 'Proje adı boş olamaz',
code: 'EMPTY_NAME',
},
{
test: name.length <= 50,
message: 'Proje adı 50 karakterden uzun olamaz',
code: 'NAME_TOO_LONG',
},
{
test: name.length >= 1,
message: 'Proje adı en az 1 karakter olmalı',
code: 'NAME_TOO_SHORT',
},
{
test: !/^\d/.test(name),
message: 'Proje adı sayı ile başlayamaz',
code: 'STARTS_WITH_NUMBER',
},
{
test: !name.includes(' '),
message: 'Proje adı boşluk içeremez',
code: 'CONTAINS_SPACE',
},
{
test: !/^[.-]/.test(name),
message: 'Proje adı nokta veya tire ile başlayamaz',
code: 'STARTS_WITH_SPECIAL',
},
{
test: !/[.-]$/.test(name),
message: 'Proje adı nokta veya tire ile bitemez',
code: 'ENDS_WITH_SPECIAL',
},
]
// Kontrolleri çalıştır
for (const check of checks) {
if (!check.test) {
return {
isValid: false,
message: check.message,
code: check.code,
}
}
}
// Karakter validasyonu (Türkçe karakterler normalize edildikten sonra)
const normalizedName = normalizeProjectName(name)
const validationRegex = /^[a-zA-Z0-9._-]+$/
if (!validationRegex.test(normalizedName)) {
return {
isValid: false,
message: 'Proje adı sadece harf, rakam, nokta, tire ve alt çizgi içerebilir',
code: 'INVALID_CHARACTERS',
}
}
// Reserved names kontrolü
const reservedNames = [
'test',
'react',
'node_modules',
'.git',
'src',
'public',
'build',
'dist',
'coverage',
'next',
'vercel',
'netlify',
'app',
'api',
'www',
'admin',
'root',
'system',
'config',
'lib',
'bin',
'tmp',
]
if (reservedNames.includes(normalizedName.toLowerCase())) {
return {
isValid: false,
message: `"${name}" adı rezerve edilmiştir, farklı bir ad seçin`,
code: 'RESERVED_NAME',
}
}
// Npm package name kontrolü
const npmInvalidPatterns = [
/^_/, // underscore ile başlayamaz
/\s/, // boşluk içeremez
/[A-Z]/, // büyük harf içeremez (npm packages)
/[@/]/, // @ veya / içeremez
]
for (const pattern of npmInvalidPatterns) {
if (pattern.test(normalizedName)) {
return {
isValid: false,
message: 'Proje adı npm package naming kurallarına uygun değil',
code: 'INVALID_NPM_NAME',
}
}
}
return { isValid: true, normalizedName }
}
/**
* Package.json dosyasını proje için özelleştiren fonksiyon
*/
async function customizePackageJson(targetDir, projectName) {
const packageJsonPath = path.join(targetDir, 'package.json')
try {
const packageJson = await fs.readJson(packageJsonPath)
// Proje bilgilerini güncelle
packageJson.name = projectName
packageJson.version = '0.1.0'
packageJson.description = `${projectName} - Starkon ile oluşturulmuş Next.js projesi`
packageJson.private = true
// CLI specific alanları kaldır
delete packageJson.bin
delete packageJson.files
delete packageJson.main
delete packageJson.module
delete packageJson.types
delete packageJson.sideEffects
// Build ve dev script'lerini güncelle
packageJson.scripts = {
dev: 'next dev',
build: 'next build',
start: 'next start',
lint: 'next lint',
'type-check': 'tsc --noEmit',
prettier: 'prettier --write "src/**/*.{js,ts,jsx,tsx}"',
'prettier:check': 'prettier --check "src/**/*.{js,ts,jsx,tsx}"',
test: 'jest',
'test:watch': 'jest --watch',
'test:coverage': 'jest --coverage',
}
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 })
return true
} catch (error) {
fileLog.error('Package.json update failed', { error: error.message, packageJsonPath })
return false
}
}
/**
* Multi-step progress tracking
*/
function createProgressTracker(steps) {
let currentStep = 0
const totalSteps = steps.length
return {
start(spinner) {
currentStep = 0
spinner.text = `[1/${totalSteps}] ${steps[currentStep]}`
},
next(spinner) {
currentStep++
if (currentStep < totalSteps) {
spinner.text = `[${currentStep + 1}/${totalSteps}] ${steps[currentStep]}`
}
},
getCurrentStep() {
return currentStep
},
getTotalSteps() {
return totalSteps
},
}
}
/**
* Template dosyalarını kopyalayan fonksiyon
*/
async function copyTemplateFiles(targetDir, templateKey = 'standard') {
const templateDir = path.join(__dirname)
const selectedTemplate = TEMPLATES[templateKey]
try {
// Base exclude files - CLI ve development dosyaları
const baseExcludeFiles = [
'index.js', // CLI dosyası
'node_modules',
'.git',
'.next',
'dist',
'coverage',
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'.DS_Store',
'Thumbs.db',
// Development/Internal dosyalar - kullanıcının görmemesi gerekenler
'CLAUDE.md', // Claude instructions
'CLI_USAGE_GUIDE.md', // CLI documentation
'.claude', // Claude settings directory
'LICENSE', // Package license
'tsup.config.ts', // Build configuration
'tsconfig.cjs.json', // CJS TypeScript config
'.npmignore', // NPM ignore rules
]
// Template'e özel exclude files
const templateExcludeFiles = selectedTemplate ? selectedTemplate.excludeFiles : []
const allExcludeFiles = [...baseExcludeFiles, ...templateExcludeFiles]
// Template dosyalarını kopyala
await fs.copy(templateDir, targetDir, {
filter: (src) => {
const relativePath = path.relative(templateDir, src)
const fileName = path.basename(src)
// Exclude listesindeki dosyaları atla
const shouldExclude = allExcludeFiles.some((exclude) => {
return (
relativePath.startsWith(exclude) ||
fileName === exclude ||
relativePath === exclude ||
relativePath.includes(exclude)
)
})
return !shouldExclude
},
})
// .gitignore dosyasını özel olarak oluştur (çünkü npm publish sırasında .gitignore dosyası ignore edilebilir)
const gitignoreContent = `# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# Testing
/coverage
*.lcov
# Next.js
/.next/
/out/
# Production
/build
/dist
# Misc
.DS_Store
*.pem
.vscode/
.idea/
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Local env files
.env
.env*.local
.env.development.local
.env.test.local
.env.production.local
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
# Turbo
.turbo
# Package lock files
package-lock.json
yarn.lock
pnpm-lock.yaml
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
`
await fs.writeFile(path.join(targetDir, '.gitignore'), gitignoreContent)
// Landing template için özel işlemler
if (templateKey === 'landing') {
// src/app/public/page.tsx içeriğini src/app/page.tsx'e kopyala
const publicPagePath = path.join(targetDir, 'src/app/public/page.tsx')
const rootPagePath = path.join(targetDir, 'src/app/page.tsx')
if (await fs.pathExists(publicPagePath)) {
await fs.copy(publicPagePath, rootPagePath)
}
// src/app/public/layout.tsx içeriğini src/app/layout.tsx'e merge et
const publicLayoutPath = path.join(targetDir, 'src/app/public/layout.tsx')
const rootLayoutPath = path.join(targetDir, 'src/app/layout.tsx')
if (await fs.pathExists(publicLayoutPath)) {
// Landing layout'u kullan
const landingLayoutContent = await fs.readFile(publicLayoutPath, 'utf8')
// Landing layout'taki LandingNavbar ve LandingFooter'ı kullan
const updatedContent = landingLayoutContent.replace(
'export default function PublicLayout',
'export default function RootLayout',
)
await fs.writeFile(rootLayoutPath, updatedContent)
}
}
return true
} catch (error) {
console.error(chalk.red('Template dosyaları kopyalanırken hata oluştu:'), error.message)
return false
}
}
/**
* Git repository'sini initialize eden fonksiyon
*/
async function initializeGit(targetDir) {
try {
const childProcess = await safeImport('child_process')
// Git repo'yu initialize et
childProcess.execSync('git init', {
cwd: targetDir,
stdio: 'ignore',
timeout: 10000, // 10 saniye timeout
})
// Initial commit
childProcess.execSync('git add .', {
cwd: targetDir,
stdio: 'ignore',
timeout: 10000,
})
childProcess.execSync('git commit -m "feat: initial commit with Starkon"', {
cwd: targetDir,
stdio: 'ignore',
timeout: 10000,
})
return true
} catch (error) {
console.warn(chalk.yellow(`⚠️ Git initialization failed: ${error.message}`))
return false
}
}
/**
* Version update kontrolü
*/
async function checkForUpdates(locale) {
try {
const currentVersion = '0.0.48'
const response = await safeFetch('https://registry.npmjs.org/starkon/latest')
const data = await response.json()
if (data.version && data.version !== currentVersion) {
console.log('')
console.log(chalk.yellow(locale.UPDATE_AVAILABLE))
updateLog.info('Update available', {
currentVersion,
latestVersion: data.version,
updateCommand: 'npm update -g starkon',
})
}
} catch {
// Network hatası durumunda sessizce devam et
}
}
/**
* Anonim telemetry gönderme (optional)
*/
async function sendTelemetry(_data) {
// Telemetry tamamen optional ve disable edilebilir
if (process.env.STARKON_TELEMETRY === 'false' || process.env.NO_TELEMETRY === '1') {
return
}
try {
// Sadece anonim kullanım istatistikleri
// const payload = {
// version: data.version,
// template: data.template,
// packageManager: data.packageManager,
// timestamp: new Date().toISOString(),
// // Hiçbir kişisel bilgi gönderilmez
// sessionId: Math.random().toString(36).substring(7),
// }
// Fake endpoint - gerçek implementasyonda gerçek endpoint kullanılır
// await fetch('https://api.starkon.com/telemetry', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(payload)
// })
} catch {
// Telemetry hatası asla kullanıcıyı etkilemez
}
}
/**
* Ana proje oluşturma fonksiyonu
*/
async function createProject(projectDir, options = {}) {
// System requirements kontrolü
await validateSystemRequirements()
// Plugin'leri yükle
await pluginManager.loadPlugins()
// Hook: beforeProjectCreate
await pluginManager.executeHook('beforeProjectCreate', { projectDir, options })
// Config yükle
const config = await loadUserConfig()
const locale = await loadLocale(config.locale)
// Version kontrolü
if (!options.skipUpdateCheck && !config.skipUpdateCheck) {
await checkForUpdates(locale)
}
createLog.info('Starting project creation', {
projectDir: projectDir || 'unspecified',
options,
locale: config.locale,
})
let projectName = projectDir
let selectedTemplate = config.defaultTemplate || 'standard'
// Proje adı verilmemişse kullanıcıdan al
if (!projectName) {
const response = await prompts({
type: 'text',
name: 'projectName',
message: locale.PROJECT_NAME_PROMPT,
initial: 'my-starkon-app',
validate: (name) => {
const result = validateProjectName(name)
return result.isValid ? true : result.message
},
})
if (!response.projectName) {
createLog.info('Project creation cancelled by user')
shutdownManager.safeExit(0)
return
}
projectName = response.projectName
}
// Hook: beforeTemplateSelect
await pluginManager.executeHook('beforeTemplateSelect', { projectName, options })
// Template seçimi (eğer options'da template belirtilmemişse)
if (!options.template) {
// Önce cache'den kontrol et
const cachedTemplate = await getCachedTemplate(selectedTemplate)
if (cachedTemplate) {
createLog.info('Template loaded from cache', { template: selectedTemplate })
}
selectedTemplate = await selectTemplate()
} else {
selectedTemplate = options.template
if (!TEMPLATES[selectedTemplate]) {
createLog.error('Invalid template specified', {
template: selectedTemplate,
availableTemplates: Object.keys(TEMPLATES),
})
shutdownManager.safeExit(1)
return
}
}
// Hook: afterTemplateSelect
await pluginManager.executeHook('afterTemplateSelect', { projectName, selectedTemplate, options })
// Template'i cache'e kaydet
await cacheTemplate(selectedTemplate, TEMPLATES[selectedTemplate])
// Proje adını validate et
const validation = validateProjectName(projectName)
if (!validation.isValid) {
createLog.error('Project name validation failed', {
projectName,
error: validation.message,
code: validation.code,
})
if (validation.code === 'INVALID_CHARACTERS' && validation.normalizedName) {
createLog.info('Project name normalization suggestion', {
original: projectName,
suggested: normalizeProjectName(projectName),
})
}
shutdownManager.safeExit(1)
return
}
// Türkçe karakterler varsa normalize et
if (validation.normalizedName !== projectName) {
createLog.info('Turkish characters normalized', {
original: projectName,
normalized: validation.normalizedName,
})
projectName = validation.normalizedName
}
const targetDir = path.resolve(process.cwd(), projectName)
// Dizin mevcut mu kontrol et
if (await fs.pathExists(targetDir)) {
const files = await fs.readdir(targetDir)
if (files.length > 0) {
const response = await prompts({
type: 'confirm',
name: 'overwrite',
message: `"${projectName}" ${locale.DIRECTORY_EXISTS}`,
initial: false,
})
if (!response.overwrite) {
createLog.info('Directory overwrite cancelled by user', { targetDir })
shutdownManager.safeExit(0)
return
}
}
}
createLog.info('Creating project directory', { projectName, targetDir })
// Progress tracker ve spinner başlat
const steps = [
locale.ANALYZING_TEMPLATES,
locale.CREATING_STRUCTURE,
locale.COPYING_FILES,
locale.CUSTOMIZING_PACKAGE,
...(options.skipGit || config.skipGit ? [] : [locale.INITIALIZING_GIT]),
locale.FINAL_CHECKS,
]
const progress = createProgressTracker(steps)
const spinner = ora().start()
progress.start(spinner)
try {
// 1. Template analizi
await new Promise((resolve) => setTimeout(resolve, 200))
progress.next(spinner)
// 2. Hedef dizini oluştur
await fs.ensureDir(targetDir)
progress.next(spinner)
// Hook: beforeFilesCopy
await pluginManager.executeHook('beforeFilesCopy', { targetDir, selectedTemplate })
// 3. Template dosyalarını kopyala
const copySuccess = await copyTemplateFiles(targetDir, selectedTemplate)
if (!copySuccess) {
throw new StarkonError('Template dosyaları kopyalanamadı', 'COPY_FAILED')
}
progress.next(spinner)
// Hook: afterFilesCopy
await pluginManager.executeHook('afterFilesCopy', { targetDir, selectedTemplate })
// 4. Package.json'ı özelleştir
const packageSuccess = await customizePackageJson(targetDir, projectName)
if (!packageSuccess) {
throw new StarkonError('Package.json güncellenemedi', 'PACKAGE_JSON_FAILED')
}
progress.next(spinner)
// 5. Git repo'yu initialize et
if (!options.skipGit && !config.skipGit) {
await initializeGit(targetDir)
progress.next(spinner)
}
// 6. Son kontroller
await new Promise((resolve) => setTimeout(resolve, 100))
if (!options.skipGit && !config.skipGit) progress.next(spinner)
spinner.succeed(chalk.green('✅ Proje başarıyla oluşturuldu!'))
// Package manager'ı detect et
const packageManager =
config.preferredPackageManager === 'auto' ? await detectPackageManager() : config.preferredPackageManager
const commands = getPackageManagerCommands(packageManager)
// Başarı mesajları
createLog.in