diff --git a/.github/workflows/test-presets.yml b/.github/workflows/test-presets.yml
new file mode 100644
index 0000000..3448f32
--- /dev/null
+++ b/.github/workflows/test-presets.yml
@@ -0,0 +1,32 @@
+name: E2E Preset Test
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ test-presets:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ preset: [landing, saas, ecommerce]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 10
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Test ${{ matrix.preset }} preset
+ run: node scripts/test-presets.mjs ${{ matrix.preset }}
diff --git a/package.json b/package.json
index 5c382c3..4bf917b 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,12 @@
"scripts": {
"dev": "pnpm --filter @coding-factory/base dev",
"build": "pnpm --filter @coding-factory/base build",
- "cli": "pnpm --filter @coding-factory/cli start"
+ "cli:build": "pnpm --filter @coding-factory/cli build",
+ "create": "node packages/cli/dist/index.js init",
+ "factory": "node packages/cli/dist/index.js",
+ "test:e2e": "node scripts/test-presets.mjs",
+ "demo:build": "node scripts/build-demo.mjs",
+ "demo:generate": "node scripts/build-demo.mjs --only-generate"
},
"engines": {
"node": ">=20",
diff --git a/packages/cli/package.json b/packages/cli/package.json
index a7bf0e4..726b240 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -12,6 +12,10 @@
"dev": "tsup src/index.ts --format esm --watch",
"start": "node dist/index.js"
},
+ "files": [
+ "dist",
+ "README.md"
+ ],
"dependencies": {
"@clack/prompts": "^0.9",
"commander": "^13",
diff --git a/scripts/build-demo.mjs b/scripts/build-demo.mjs
new file mode 100644
index 0000000..fdcf48b
--- /dev/null
+++ b/scripts/build-demo.mjs
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+
+/**
+ * Build Demo Site
+ *
+ * Generates a demo project from starters/base + starters/demo pages,
+ * ready for deployment to Vercel or any static host.
+ *
+ * Usage:
+ * node scripts/build-demo.mjs # generate + build
+ * node scripts/build-demo.mjs --only-generate # generate only (no build)
+ */
+
+import { cpSync, mkdirSync, existsSync, rmSync } from 'node:fs'
+import { resolve, dirname } from 'node:path'
+import { execSync } from 'node:child_process'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = dirname(fileURLToPath(import.meta.url))
+const ROOT = resolve(__dirname, '..')
+const DEMO_OUT = resolve(ROOT, '..', '.coding-factory-demo')
+
+const onlyGenerate = process.argv.includes('--only-generate')
+
+console.log('Building Coding Factory demo site...\n')
+
+// Clean
+if (existsSync(DEMO_OUT)) rmSync(DEMO_OUT, { recursive: true, force: true })
+
+// Copy base starter
+console.log('1. Copying base starter...')
+cpSync(resolve(ROOT, 'starters/base'), DEMO_OUT, { recursive: true })
+
+// Overlay demo pages
+console.log('2. Overlaying demo pages...')
+cpSync(resolve(ROOT, 'starters/demo/src'), resolve(DEMO_OUT, 'src'), { recursive: true, force: true })
+
+// Install
+console.log('3. Installing dependencies...')
+execSync('pnpm install --no-frozen-lockfile', {
+ cwd: DEMO_OUT,
+ stdio: 'inherit',
+ timeout: 120_000,
+})
+
+if (!onlyGenerate) {
+ // Build
+ console.log('\n4. Building...')
+ execSync('pnpm exec next build', {
+ cwd: DEMO_OUT,
+ stdio: 'inherit',
+ timeout: 180_000,
+ })
+}
+
+console.log(`\nDemo site ready at: ${DEMO_OUT}`)
+console.log('Run: cd ' + DEMO_OUT + ' && pnpm dev')
diff --git a/scripts/test-presets.mjs b/scripts/test-presets.mjs
new file mode 100644
index 0000000..8210d9f
--- /dev/null
+++ b/scripts/test-presets.mjs
@@ -0,0 +1,206 @@
+#!/usr/bin/env node
+
+/**
+ * E2E Preset Test Script
+ *
+ * Generates a project for each preset, installs deps, and runs `next build`.
+ * Exit code 0 = all passed, 1 = at least one failed.
+ *
+ * Usage:
+ * node scripts/test-presets.mjs # test all presets
+ * node scripts/test-presets.mjs landing # test single preset
+ * node scripts/test-presets.mjs --keep # don't clean up after test
+ */
+
+import { cpSync, mkdirSync, existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
+import { resolve, dirname } from 'node:path'
+import { execSync } from 'node:child_process'
+import { fileURLToPath } from 'node:url'
+
+const __dirname = dirname(fileURLToPath(import.meta.url))
+const ROOT = resolve(__dirname, '..')
+const TEST_DIR = resolve(ROOT, '..', '.coding-factory-test-output')
+
+// Parse args
+const args = process.argv.slice(2)
+const keepOutput = args.includes('--keep')
+const targetPresets = args.filter((a) => !a.startsWith('--'))
+
+// Load registry
+const registry = JSON.parse(readFileSync(resolve(ROOT, 'registry/registry.json'), 'utf-8'))
+const presetNames = targetPresets.length > 0 ? targetPresets : Object.keys(registry.presets)
+
+// Resolve module dependencies
+function resolveModuleDeps(moduleNames) {
+ const resolved = []
+ const visited = new Set()
+ function walk(name) {
+ if (visited.has(name)) return
+ visited.add(name)
+ const mod = registry.modules[name]
+ if (!mod) return
+ for (const dep of mod.requiredModules || []) walk(dep)
+ resolved.push(name)
+ }
+ for (const name of moduleNames) walk(name)
+ return resolved
+}
+
+// Generate a project from preset
+function generateProject(presetName) {
+ const preset = registry.presets[presetName]
+ if (!preset) {
+ console.error(` Unknown preset: ${presetName}`)
+ return null
+ }
+
+ const targetDir = resolve(TEST_DIR, `${presetName}-project`)
+ if (existsSync(targetDir)) rmSync(targetDir, { recursive: true, force: true })
+
+ // Copy base starter
+ cpSync(resolve(ROOT, 'starters/base'), targetDir, { recursive: true })
+
+ // Resolve and apply modules
+ const modules = resolveModuleDeps(preset.modules)
+
+ for (const modName of modules) {
+ const manifestPath = resolve(ROOT, 'registry/modules', modName, 'module.json')
+ if (!existsSync(manifestPath)) continue
+
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
+
+ // Copy files
+ for (const f of manifest.files) {
+ const src = resolve(ROOT, 'registry/modules', modName, f.source)
+ const dest = resolve(targetDir, f.target)
+ if (existsSync(src)) {
+ mkdirSync(dirname(dest), { recursive: true })
+ cpSync(src, dest, { recursive: true, force: true })
+ }
+ }
+
+ // Merge dependencies
+ const deps = manifest.dependencies || {}
+ const devDeps = manifest.devDependencies || {}
+ if (Object.keys(deps).length > 0 || Object.keys(devDeps).length > 0) {
+ const pkgPath = resolve(targetDir, 'package.json')
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
+ pkg.dependencies = { ...(pkg.dependencies || {}), ...deps }
+ pkg.devDependencies = { ...(pkg.devDependencies || {}), ...devDeps }
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
+ }
+ }
+
+ // Write factory config
+ writeFileSync(
+ resolve(targetDir, 'factory.config.json'),
+ JSON.stringify({ preset: presetName, theme: 'default', modules, layout: preset.layout }, null, 2) + '\n'
+ )
+
+ return { targetDir, modules }
+}
+
+// Run build for a project
+function buildProject(presetName, targetDir) {
+ try {
+ // Install deps (allow build scripts for prisma etc.)
+ execSync('pnpm install --no-frozen-lockfile', {
+ cwd: targetDir,
+ stdio: 'pipe',
+ timeout: 120_000,
+ env: { ...process.env, npm_config_ignore_scripts: '' },
+ })
+
+ // Prisma generate if db module is present
+ if (existsSync(resolve(targetDir, 'prisma/schema.prisma'))) {
+ const prismaBin = resolve(targetDir, 'node_modules/.bin/prisma')
+ if (existsSync(prismaBin)) {
+ execSync(`${prismaBin} generate`, {
+ cwd: targetDir,
+ stdio: 'pipe',
+ timeout: 30_000,
+ })
+ }
+ }
+
+ // Next.js build
+ execSync('pnpm exec next build', {
+ cwd: targetDir,
+ stdio: 'pipe',
+ timeout: 180_000,
+ })
+
+ return { success: true }
+ } catch (error) {
+ const stderr = error.stderr?.toString() || ''
+ const stdout = error.stdout?.toString() || ''
+ return { success: false, error: stderr || stdout }
+ }
+}
+
+// Main
+console.log('╔══════════════════════════════════════╗')
+console.log('║ Coding Factory — E2E Preset Test ║')
+console.log('╚══════════════════════════════════════╝')
+console.log()
+
+// Ensure CLI is built
+console.log('Building CLI...')
+execSync('pnpm --filter @coding-factory/cli build', { cwd: ROOT, stdio: 'pipe' })
+
+// Clean test output
+if (existsSync(TEST_DIR)) rmSync(TEST_DIR, { recursive: true, force: true })
+mkdirSync(TEST_DIR, { recursive: true })
+
+const results = []
+
+for (const presetName of presetNames) {
+ console.log(`\n━━━ ${presetName.toUpperCase()} ━━━`)
+
+ // Generate
+ process.stdout.write(' Generating... ')
+ const project = generateProject(presetName)
+ if (!project) {
+ results.push({ preset: presetName, success: false, error: 'Generation failed' })
+ console.log('FAIL')
+ continue
+ }
+ console.log(`OK (${project.modules.length} modules)`)
+
+ // Build
+ process.stdout.write(' Installing + Building... ')
+ const buildResult = buildProject(presetName, project.targetDir)
+
+ if (buildResult.success) {
+ console.log('OK ✓')
+ results.push({ preset: presetName, success: true })
+ } else {
+ console.log('FAIL ✗')
+ // Show last 10 lines of error
+ const errorLines = buildResult.error.split('\n').filter(Boolean).slice(-10)
+ for (const line of errorLines) {
+ console.log(` ${line}`)
+ }
+ results.push({ preset: presetName, success: false, error: buildResult.error })
+ }
+}
+
+// Summary
+console.log('\n━━━ RESULTS ━━━')
+const passed = results.filter((r) => r.success).length
+const failed = results.filter((r) => !r.success).length
+
+for (const r of results) {
+ console.log(` ${r.success ? '✓' : '✗'} ${r.preset}`)
+}
+console.log(`\n ${passed} passed, ${failed} failed`)
+
+// Cleanup
+if (!keepOutput && failed === 0) {
+ rmSync(TEST_DIR, { recursive: true, force: true })
+ console.log(' (test output cleaned)')
+} else if (failed > 0) {
+ console.log(` (test output kept at ${TEST_DIR})`)
+}
+
+process.exit(failed > 0 ? 1 : 0)
diff --git a/starters/demo/src/app/demo/ecommerce/page.tsx b/starters/demo/src/app/demo/ecommerce/page.tsx
new file mode 100644
index 0000000..bf9e114
--- /dev/null
+++ b/starters/demo/src/app/demo/ecommerce/page.tsx
@@ -0,0 +1,109 @@
+import Link from 'next/link'
+
+const products = [
+ { name: 'Basic Plan', price: '₩29,000/mo', desc: 'For individuals', features: ['5 projects', 'All modules', 'Email support'] },
+ { name: 'Pro Plan', price: '₩79,000/mo', desc: 'For teams', features: ['Unlimited projects', 'Priority support', 'Custom themes'] },
+ { name: 'Enterprise', price: 'Contact us', desc: 'For organizations', features: ['White-label', 'Dedicated support', 'Custom modules'] },
+]
+
+export default function EcommerceDemo() {
+ return (
+
+
+
+ {/* Hero */}
+
+ E-commerce with Payments
+
+ Full-stack e-commerce ready: authentication, database, security, SEO, analytics, ads,
+ and payment gateway integration out of the box.
+
+
+
+ {/* Pricing Cards (demonstrate payment flow) */}
+
+ Pricing Example
+
+ {products.map((product, i) => (
+
+ {i === 1 && (
+
+ Popular
+
+ )}
+
{product.name}
+
{product.desc}
+
{product.price}
+
+ {product.features.map((f) => (
+ -
+ ✓ {f}
+
+ ))}
+
+
+
+ ))}
+
+
+
+ {/* Payment Flow */}
+
+ Payment Integration
+
+
{'// Use PaymentButton anywhere in your app'}
+
{'import { PaymentButton } from'} {'"@/components/payments/payment-button"'}
+
{'
+ {' orderId="order-123"'}
+ {' amount={29000}'}
+ {' orderName="Pro Plan"'}
+ {'>'}
+ {' Subscribe Now'}
+ {'
'}
+
{'// Supports Toss Payments (KR) and Stripe (Global)'}
+
{'// Switch with PAYMENT_PROVIDER=toss|stripe'}
+
+
+
+ {/* All 7 Modules */}
+
+ All 7 Modules Included
+
+ {[
+ { name: 'Auth', desc: 'Social + credentials login with NextAuth.js v5' },
+ { name: 'Database', desc: 'Prisma ORM with PostgreSQL' },
+ { name: 'Security', desc: 'CSP, CSRF, rate limiting, Zod validation' },
+ { name: 'SEO', desc: 'Meta tags, sitemap, structured data' },
+ { name: 'Analytics', desc: 'GA4, GTM, event tracking' },
+ { name: 'Ads', desc: 'AdSense, AdPost ad placement' },
+ { name: 'Payments', desc: 'Toss / Stripe unified adapter' },
+ ].map((mod) => (
+
+
✓
+
+
{mod.name}
+
{mod.desc}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/starters/demo/src/app/demo/landing/page.tsx b/starters/demo/src/app/demo/landing/page.tsx
new file mode 100644
index 0000000..496be7b
--- /dev/null
+++ b/starters/demo/src/app/demo/landing/page.tsx
@@ -0,0 +1,80 @@
+import Link from 'next/link'
+
+export default function LandingDemo() {
+ return (
+
+
+
+ {/* Hero Section */}
+
+
+ Your Product, Supercharged
+
+
+ A beautiful landing page with built-in SEO optimization, analytics tracking,
+ and ad placement — all configured automatically.
+
+
+
+
+
+
+
+ {/* Included Modules */}
+
+ What's Included
+
+
+
+ component', 'Ad-block detection']}
+ />
+
+
+
+ {/* Code Example */}
+
+ One Command
+
+
# Generate a landing page project
+
pnpm create my-landing
+
# Modules are already configured:
+
✓ SEO — meta tags, sitemap, structured data
+
✓ Analytics — GA4, GTM, event tracking
+
✓ Ads — AdSense, AdPost slots
+
+
+
+ )
+}
+
+function ModuleCard({ title, features }: { title: string; features: string[] }) {
+ return (
+
+
{title}
+
+ {features.map((f) => (
+ - • {f}
+ ))}
+
+
+ )
+}
diff --git a/starters/demo/src/app/demo/saas/page.tsx b/starters/demo/src/app/demo/saas/page.tsx
new file mode 100644
index 0000000..1def9a7
--- /dev/null
+++ b/starters/demo/src/app/demo/saas/page.tsx
@@ -0,0 +1,71 @@
+import Link from 'next/link'
+
+export default function SaasDemo() {
+ return (
+
+
+
+ {/* Dashboard Preview */}
+
+ {/* Sidebar */}
+
+
+ {/* Main Content */}
+
+
SaaS Dashboard
+
+ {/* Stats */}
+
+ {[
+ { label: 'Total Users', value: '12,847' },
+ { label: 'Active Now', value: '342' },
+ { label: 'Revenue', value: '$48,290' },
+ { label: 'Growth', value: '+12.5%' },
+ ].map((stat) => (
+
+
{stat.label}
+
{stat.value}
+
+ ))}
+
+
+ {/* Included Modules */}
+
Included Modules
+
+ {[
+ { name: 'Auth', desc: 'NextAuth.js v5 — Google, Kakao, Naver + credentials login' },
+ { name: 'Database', desc: 'Prisma ORM — PostgreSQL with adapter pattern' },
+ { name: 'Security', desc: 'CSP headers, CSRF protection, rate limiting, Zod validation' },
+ { name: 'Analytics', desc: 'GA4 + GTM event tracking with cookie consent' },
+ { name: 'SEO', desc: 'Meta tags, sitemap, robots.txt, structured data' },
+ ].map((mod) => (
+
+
{mod.name}
+
{mod.desc}
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/starters/demo/src/app/layout.tsx b/starters/demo/src/app/layout.tsx
new file mode 100644
index 0000000..49e41e1
--- /dev/null
+++ b/starters/demo/src/app/layout.tsx
@@ -0,0 +1,15 @@
+import type { Metadata } from 'next'
+import './globals.css'
+
+export const metadata: Metadata = {
+ title: 'Coding Factory — Demo',
+ description: '모듈 조합으로 웹 프로젝트를 빠르게 생성하는 보일러플레이트 시스템',
+}
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/starters/demo/src/app/page.tsx b/starters/demo/src/app/page.tsx
new file mode 100644
index 0000000..1a94a80
--- /dev/null
+++ b/starters/demo/src/app/page.tsx
@@ -0,0 +1,154 @@
+import Link from 'next/link'
+
+const presets = [
+ {
+ name: 'Landing',
+ description: 'SEO + Analytics + Ads',
+ modules: ['seo', 'analytics', 'ads'],
+ href: '/demo/landing',
+ color: 'bg-blue-500',
+ },
+ {
+ name: 'SaaS',
+ description: 'Auth + DB + Security + Analytics + SEO',
+ modules: ['auth', 'db', 'security', 'analytics', 'seo'],
+ href: '/demo/saas',
+ color: 'bg-purple-500',
+ },
+ {
+ name: 'E-commerce',
+ description: 'Auth + DB + Security + SEO + Analytics + Ads + Payments',
+ modules: ['auth', 'db', 'security', 'seo', 'analytics', 'ads', 'payments'],
+ href: '/demo/ecommerce',
+ color: 'bg-green-500',
+ },
+]
+
+const modules = [
+ { name: 'seo', description: 'Meta tags, sitemap, robots.txt, JSON-LD', icon: '🔍' },
+ { name: 'analytics', description: 'GA4, GTM, event tracking, cookie consent', icon: '📊' },
+ { name: 'ads', description: 'Google AdSense, Naver AdPost', icon: '📢' },
+ { name: 'security', description: 'CSP, CSRF, rate limiting, input validation', icon: '🛡️' },
+ { name: 'auth', description: 'NextAuth.js v5, social + credentials login', icon: '🔐' },
+ { name: 'db', description: 'Prisma adapter (PostgreSQL, MySQL, SQLite)', icon: '🗄️' },
+ { name: 'payments', description: 'Toss Payments / Stripe with adapter pattern', icon: '💳' },
+]
+
+export default function DemoHome() {
+ return (
+
+ {/* Hero */}
+
+
+ v0.1.0
+
+
+ Coding Factory
+
+
+ 모듈 조합으로 웹 프로젝트를 빠르게 생성하는 보일러플레이트 시스템.
+ CLI 한 줄로 프로젝트 생성, 필요한 모듈만 추가/제거.
+
+
+ {/* CLI example */}
+
+
+
+
+
+ terminal
+
+
+
$ pnpm create my-project
+
? Select a preset
+
● landing
+
○ saas
+
○ ecommerce
+
○ custom
+
+
+
+
+ {/* Presets */}
+
+ Presets
+
+ 프로젝트 유��에 맞는 프리셋을 선택하세요
+
+
+ {presets.map((preset) => (
+
+
+
+ {preset.name}
+
+
{preset.description}
+
+ {preset.modules.map((mod) => (
+
+ {mod}
+
+ ))}
+
+
+ ))}
+
+
+
+ {/* Modules */}
+
+ Modules
+
+ 7개 모듈을 자유롭게 조합하세요
+
+
+ {modules.map((mod) => (
+
+
{mod.icon}
+
{mod.name}
+
{mod.description}
+
+ ))}
+
+
+
+ {/* How it works */}
+
+ How it works
+
+ {[
+ { step: '1', title: 'Choose a preset', desc: 'landing, saas, ecommerce, or custom' },
+ { step: '2', title: 'Pick a theme', desc: 'default, corporate, or playful' },
+ { step: '3', title: 'Generate', desc: 'CLI copies code into a clean Next.js project' },
+ { step: '4', title: 'Customize', desc: 'Add/remove modules anytime with factory add/remove' },
+ ].map((item) => (
+
+
+ {item.step}
+
+
+
{item.title}
+
{item.desc}
+
+
+ ))}
+
+
+
+ {/* Footer */}
+
+
+ )
+}