@entro314labs/at3-stack-kit
Version:
Upgrade existing projects to AT3 Stack with intelligent migration
1 lines • 178 kB
Source Map (JSON)
{"version":3,"sources":["../src/index.ts","../src/cli.ts","../src/detect.ts","../src/features/add-ai.ts","../src/features/add-better-auth.ts","../src/utils/cli-styling.ts","../src/features/add-clerk.ts","../src/features/add-drizzle.ts","../src/features/add-i18n.ts","../src/features/add-pwa.ts","../src/features/add-supabase.ts","../src/features/add-testing.ts","../src/utils/integration.ts"],"sourcesContent":["/**\n * AT3 Stack Kit\n * CLI tool to upgrade existing projects to AT3 Stack with AI, edge, and modern features\n */\n\n// CLI utilities\nexport { program } from './cli.js'\n// Core detection and analysis\nexport * from './detect.js'\n// Feature addition modules\nexport * from './features/index.js'\n","#!/usr/bin/env node\n\n/**\n * AT3 Stack Kit CLI\n * Upgrade existing projects to AT3 Stack with AI, edge, and modern features\n */\n\nimport {\n cancel,\n confirm,\n intro,\n isCancel,\n multiselect,\n note,\n outro,\n select,\n spinner,\n} from '@clack/prompts'\nimport chalk from 'chalk'\nimport { program } from 'commander'\nimport { detect as detectPackageManager } from 'detect-package-manager'\nimport { existsSync, readFileSync } from 'fs-extra'\nimport { join } from 'path'\nimport { detectProjectType, type ProjectType } from './detect.js'\nimport { addAI, addBetterAuth, addClerk, addDrizzle, addI18n, addPWA, addSupabase, addTesting } from './features/index.js'\nimport { colors, header, style, symbols } from './utils/cli-styling.js'\nimport { getPostMigrationWorkflow, suggestAT3Tools, updateAT3Config } from './utils/integration.js'\nimport spawn from 'cross-spawn'\n\nconst features = [\n {\n id: 'ai-custom',\n name: 'AI Integration (Custom)',\n description: 'Add custom AI integration with multiple providers',\n value: 'ai-custom',\n },\n {\n id: 'ai-vercel',\n name: 'AI Integration (Vercel SDK)',\n description: 'Add Vercel AI SDK for streaming responses',\n value: 'ai-vercel',\n },\n {\n id: 'supabase',\n name: 'Supabase',\n description: 'Add Supabase for database, auth, and edge functions',\n value: 'supabase',\n },\n {\n id: 'drizzle',\n name: 'Drizzle ORM + PostgreSQL',\n description: 'Add Drizzle ORM with generic PostgreSQL support',\n value: 'drizzle',\n },\n {\n id: 'clerk',\n name: 'Clerk Auth',\n description: 'Add Clerk authentication',\n value: 'clerk',\n },\n {\n id: 'better-auth',\n name: 'Better Auth',\n description: 'Add Better Auth (Self-hosted auth)',\n value: 'better-auth',\n },\n {\n id: 'pwa',\n name: 'PWA Support',\n description: 'Add Progressive Web App features',\n value: 'pwa',\n },\n {\n id: 'i18n',\n name: 'Internationalization',\n description: 'Add next-intl for multi-language support',\n value: 'i18n',\n },\n {\n id: 'testing',\n name: 'Testing Suite',\n description: 'Add Vitest and Playwright for comprehensive testing',\n value: 'testing',\n },\n] as const\n\ntype FeatureId = (typeof features)[number]['value']\n\nasync function main() {\n console.clear()\n\n intro(style.title('AT3 Stack Kit'))\n\n // Detect current project\n const s = spinner()\n s.start('Analyzing your project...')\n\n const projectType = await detectProjectType(process.cwd())\n const packageManager = await detectPackageManager().catch(() => 'npm')\n\n s.stop(\n `${symbols.success} Detected: ${style.value(getProjectTypeLabel(projectType))} project using ${style.accent(packageManager)}`\n )\n\n if (projectType === 'unknown') {\n cancel(\n \"This doesn't appear to be a supported project type. AT3 Kit supports Next.js, T3, and React projects.\"\n )\n process.exit(1)\n }\n\n // Show current project info\n note(getProjectInfo(projectType), 'Current Project')\n\n // Select features to add\n const selectedFeatures = await multiselect({\n message: 'Which features would you like to add?',\n options: features.map((feature) => ({\n value: feature.value,\n label: feature.name,\n hint: feature.description,\n })),\n required: true,\n })\n\n if (isCancel(selectedFeatures) || selectedFeatures.length === 0) {\n cancel('No features selected.')\n process.exit(0)\n }\n\n // Confirm installation\n const shouldInstall = await confirm({\n message: `Install dependencies with ${packageManager}?`,\n initialValue: true,\n })\n\n if (isCancel(shouldInstall)) {\n cancel('Operation cancelled.')\n process.exit(0)\n }\n\n // Apply features\n const addSpinner = spinner()\n\n try {\n for (const featureId of selectedFeatures as FeatureId[]) {\n addSpinner.start(`Adding ${getFeatureName(featureId)}...`)\n\n switch (featureId) {\n case 'ai-custom':\n await addAI('custom', process.cwd())\n break\n case 'ai-vercel':\n await addAI('vercel', process.cwd())\n break\n case 'supabase':\n await addSupabase(process.cwd())\n break\n case 'drizzle':\n await addDrizzle(process.cwd())\n break\n case 'clerk':\n await addClerk(process.cwd())\n break\n case 'better-auth':\n await addBetterAuth(process.cwd())\n break\n case 'pwa':\n await addPWA(process.cwd())\n break\n case 'i18n':\n await addI18n(process.cwd())\n break\n case 'testing':\n await addTesting(process.cwd())\n break\n }\n\n addSpinner.stop(`✓ Added ${getFeatureName(featureId)}`)\n }\n\n if (shouldInstall) {\n addSpinner.start(`Installing dependencies with ${packageManager}...`)\n\n await new Promise<void>((resolve, reject) => {\n const child = spawn(packageManager, ['install'], {\n cwd: process.cwd(),\n stdio: 'ignore',\n })\n\n child.on('close', (code) => {\n if (code === 0) {\n resolve()\n } else {\n reject(new Error(`Installation failed with code ${code}`))\n }\n })\n })\n\n addSpinner.stop('✓ Dependencies installed')\n }\n\n // Update AT3 configuration and get suggestions\n const projectInfo = { type: projectType, packageManager } as any // Simplified for now\n const addedFeatures = selectedFeatures // This would be the actual added features\n\n updateAT3Config(process.cwd(), addedFeatures)\n const toolSuggestions = suggestAT3Tools(projectInfo, addedFeatures)\n const workflows = getPostMigrationWorkflow(projectInfo, addedFeatures)\n\n outro(style.success('🎉 Your project has been upgraded to AT3 Stack!'))\n\n const nextSteps = [\n 'Review the added files and configurations',\n 'Update your .env.local with required API keys',\n `Run ${style.command(packageManager + ' dev')} to test your upgraded project`,\n ...workflows,\n ]\n\n note(\n `\n${header.section('Next steps:')}\n${nextSteps.map((step, i) => ` ${colors.muted(`${i + 1}.`)} ${step}`).join('\\n')}\n\n${colors.primary('Documentation:')}\n ${colors.muted('•')} AT3 Stack Guide: https://at3-stack.dev/docs\n ${colors.muted('•')} Feature Guides: https://at3-stack.dev/docs/features\n\n${\n toolSuggestions.length > 0\n ? `${colors.primary('AT3 Ecosystem:')}\n ${toolSuggestions.map((s) => `${colors.muted('•')} ${s}`).join('\\n ')}\n\n`\n : ''\n}${colors.primary('Need help?')}\n ${colors.muted('•')} GitHub Issues: https://github.com/entro314-labs/at3-stack-kit/issues\n ${colors.muted('•')} Discord: https://discord.gg/at3-stack\n `,\n 'Welcome to AT3!'\n )\n } catch (error) {\n addSpinner.stop('Error occurred')\n console.error(chalk.red('Error upgrading project:'), error)\n process.exit(1)\n }\n}\n\nfunction getProjectTypeLabel(type: ProjectType): string {\n switch (type) {\n case 'nextjs':\n return 'Next.js'\n case 't3':\n return 'T3 Stack'\n case 'react':\n return 'React'\n case 'unknown':\n return 'Unknown'\n default:\n return 'Unknown'\n }\n}\n\nfunction getProjectInfo(type: ProjectType): string {\n const packageJsonPath = join(process.cwd(), 'package.json')\n\n if (!existsSync(packageJsonPath)) {\n return 'No package.json found'\n }\n\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n const deps = Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies })\n\n let info = `Project: ${packageJson.name || 'unnamed'}\\n`\n info += `Type: ${getProjectTypeLabel(type)}\\n`\n\n // Show key dependencies\n const keyDeps = ['next', 'react', 'typescript', 'tailwindcss']\n const foundDeps = keyDeps.filter((dep) => deps.some((d) => d.includes(dep)))\n\n if (foundDeps.length > 0) {\n info += `Dependencies: ${foundDeps.join(', ')}`\n }\n\n return info\n}\n\nfunction getFeatureName(id: FeatureId): string {\n return features.find((f) => f.value === id)?.name || id\n}\n\n// CLI setup\nprogram\n .name('at3-kit')\n .description('Upgrade existing projects to AT3 Stack')\n .version('0.1.0')\n .option('-d, --dry-run', 'Show what would be changed without making changes')\n .option('--no-install', 'Skip installing dependencies')\n .action(async (options: { dryRun?: boolean; install?: boolean }) => {\n await main()\n })\n\n// Subcommands\nprogram\n .command('add <feature>')\n .description('Add a specific feature to your project')\n .option('--no-install', 'Skip installing dependencies')\n .action(async (feature: string, options: { install?: boolean }) => {\n const validFeatures = features.map((f) => f.value)\n\n if (!validFeatures.includes(feature as FeatureId)) {\n console.error(chalk.red(`Unknown feature: ${feature}`))\n console.log(chalk.dim(`Valid features: ${validFeatures.join(', ')}`))\n process.exit(1)\n }\n\n // Add single feature without prompts\n console.log(chalk.blue(`Adding ${getFeatureName(feature as FeatureId)}...`))\n\n try {\n switch (feature) {\n case 'ai-custom':\n await addAI('custom', process.cwd())\n break\n case 'ai-vercel':\n await addAI('vercel', process.cwd())\n break\n case 'supabase':\n await addSupabase(process.cwd())\n break\n case 'drizzle':\n await addDrizzle(process.cwd())\n break\n case 'clerk':\n await addClerk(process.cwd())\n break\n case 'better-auth':\n await addBetterAuth(process.cwd())\n break\n case 'pwa':\n await addPWA(process.cwd())\n break\n case 'i18n':\n await addI18n(process.cwd())\n break\n case 'testing':\n await addTesting(process.cwd())\n break\n }\n\n if (options.install !== false) {\n console.log(chalk.blue('Installing dependencies...'))\n const packageManager = await detectPackageManager().catch(() => 'npm')\n\n await new Promise<void>((resolve, reject) => {\n const child = spawn(packageManager, ['install'], {\n cwd: process.cwd(),\n stdio: 'inherit',\n })\n\n child.on('close', (code) => {\n if (code === 0) {\n resolve()\n } else {\n reject(new Error(`Installation failed with code ${code}`))\n }\n })\n })\n }\n\n console.log(chalk.green(`✓ Successfully added ${getFeatureName(feature as FeatureId)}`))\n } catch (error) {\n console.error(chalk.red('Failed to add feature:'), error)\n process.exit(1)\n }\n })\n\nprogram\n .command('detect')\n .description('Detect current project type and show available upgrades')\n .action(async () => {\n const projectType = await detectProjectType(process.cwd())\n const packageManager = await detectPackageManager().catch(() => 'npm')\n\n console.log(chalk.green('Project Analysis:'))\n console.log(` Type: ${getProjectTypeLabel(projectType)}`)\n console.log(` Package Manager: ${packageManager}`)\n console.log('\\n' + getProjectInfo(projectType))\n })\n\n// Parse command line arguments (run as CLI)\nprogram.parse()\n\nexport { program }\n","/**\n * Project detection utilities for AT3 Stack Kit\n * Aligned with at3-toolkit's sophisticated detection system\n */\n\nimport { detect as detectPackageManager } from 'detect-package-manager'\nimport { existsSync, readdirSync, readFileSync } from 'fs-extra'\nimport { join } from 'path'\n\nexport type ProjectType =\n | 'ait3e'\n | 'nextjs'\n | 't3'\n | 'react'\n | 'vue'\n | 'nuxt'\n | 'vite'\n | 'node'\n | 'webpack'\n | 'unknown'\n\nexport type AuthProvider = 'supabase' | 'clerk' | 'better-auth' | 'next-auth' | 'lucia' | 'none'\n\nexport interface DependencyInfo {\n name: string\n version: string\n type: 'dependency' | 'devDependency' | 'peerDependency'\n current?: string // Installed version\n latest?: string // Latest available version\n}\n\nexport interface TestingInfo {\n unit: 'vitest' | 'jest' | 'none'\n e2e: 'playwright' | 'cypress' | 'none'\n}\n\nexport interface ProjectInfo {\n path: string\n type: ProjectType\n hasNextjs: boolean\n hasReact: boolean\n hasVue: boolean\n hasTypeScript: boolean\n hasTailwind: boolean\n hasTRPC: boolean\n hasSupabase: boolean\n hasAI: boolean\n hasPWA: boolean\n hasI18n: boolean\n hasVitest: boolean\n hasPlaywright: boolean\n hasEslint: boolean\n hasPrettier: boolean\n hasBiome: boolean\n hasEdgeRuntime: boolean\n hasVectorDB: boolean\n // Extended detection\n hasDrizzle: boolean\n hasPrisma: boolean\n authProvider: AuthProvider\n testing: TestingInfo\n packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun'\n dependencies: DependencyInfo[]\n configFiles: string[]\n}\n\n/**\n * Detect project type based on dependencies and file structure\n * Enhanced with comprehensive type detection from at3-toolkit\n */\nexport async function detectProjectType(projectPath: string): Promise<ProjectType> {\n const packageJsonPath = join(projectPath, 'package.json')\n\n if (!existsSync(packageJsonPath)) {\n return 'unknown'\n }\n\n try {\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }\n\n // Check for AIT3E stack (AI + T3 + Edge)\n const hasAI =\n deps.ai || deps['@ai-sdk/openai'] || deps['@ai-sdk/anthropic'] || deps['@ai-sdk/google']\n const hasSupabase = deps['@supabase/supabase-js'] || deps['@supabase/ssr']\n const hasNextJS = deps.next\n const hasTailwind = deps.tailwindcss\n const hasTypeScript = deps.typescript || existsSync(join(projectPath, 'tsconfig.json'))\n\n if (hasAI && hasSupabase && hasNextJS && (hasTailwind || hasTypeScript)) {\n return 'ait3e'\n }\n\n // Check for T3 stack (Next.js + tRPC + Prisma/Drizzle)\n if (deps.next && deps['@trpc/server']) {\n if (deps.prisma || deps['@prisma/client'] || deps['drizzle-orm']) {\n return 't3'\n }\n }\n\n // Check for Next.js\n if (deps.next) return 'nextjs'\n\n // Check for Nuxt\n if (deps.nuxt || deps['@nuxt/core']) return 'nuxt'\n\n // Check for Vue\n if (deps.vue) return 'vue'\n\n // Check for React\n if (deps.react) return 'react'\n\n // Check for Vite\n if (deps.vite) return 'vite'\n\n // Check for Webpack\n if (deps.webpack) return 'webpack'\n\n // Default to Node.js\n return 'node'\n } catch (error) {\n return 'unknown'\n }\n}\n\n/**\n * Get comprehensive project information\n * Enhanced with sophisticated dependency analysis from at3-toolkit\n */\nexport async function analyzeProject(projectPath: string): Promise<ProjectInfo> {\n if (!existsSync(projectPath)) {\n throw new Error(`Project path does not exist: ${projectPath}`)\n }\n\n const packageJsonPath = join(projectPath, 'package.json')\n if (!existsSync(packageJsonPath)) {\n throw new Error('No package.json found. This does not appear to be a Node.js project.')\n }\n\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))\n\n // Use sophisticated package manager detection with fallback\n let packageManager: ProjectInfo['packageManager'] = 'npm'\n try {\n packageManager = (await detectPackageManager({ cwd: projectPath })) as any\n } catch {\n // Fallback to file-based detection\n if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) packageManager = 'pnpm'\n else if (existsSync(join(projectPath, 'yarn.lock'))) packageManager = 'yarn'\n else if (existsSync(join(projectPath, 'bun.lockb'))) packageManager = 'bun'\n }\n\n // Analyze dependencies with detailed structure\n const dependencies = await analyzeDependencies(packageJson, projectPath)\n const configFiles = findConfigFiles(projectPath)\n\n // Feature detection\n const hasTypeScript = hasTypeScriptSupport(projectPath, dependencies)\n const hasNextjs = hasDependency(dependencies, 'next')\n const hasReact = hasDependency(dependencies, 'react')\n const hasVue = hasDependency(dependencies, 'vue')\n const hasTailwind = hasDependency(dependencies, 'tailwindcss')\n const hasTRPC = detectTRPC(dependencies)\n const hasEslint = hasDependency(dependencies, 'eslint')\n const hasPrettier = hasDependency(dependencies, 'prettier')\n const hasBiome = hasDependency(dependencies, '@biomejs/biome')\n\n // AT3-specific features\n const hasAI = detectAISupport(dependencies)\n const hasSupabase = detectSupabase(dependencies, projectPath)\n const hasEdgeRuntime = detectEdgeRuntime(projectPath)\n const hasVectorDB = hasSupabaseVectorConfig(projectPath)\n const hasPWA = detectPWA(dependencies, projectPath)\n const hasI18n = detectI18n(dependencies, projectPath)\n\n // Database detection\n const hasDrizzle = detectDrizzle(dependencies, projectPath)\n const hasPrisma = detectPrisma(dependencies, projectPath)\n\n // Auth detection\n const authProvider = detectAuthProvider(dependencies, projectPath)\n\n // Testing detection\n const testing = detectTesting(dependencies)\n const hasVitest = testing.unit === 'vitest'\n const hasPlaywright = testing.e2e === 'playwright'\n\n return {\n path: projectPath,\n type: await detectProjectType(projectPath),\n hasNextjs,\n hasReact,\n hasVue,\n hasTypeScript,\n hasTailwind,\n hasTRPC,\n hasSupabase,\n hasAI,\n hasPWA,\n hasI18n,\n hasVitest,\n hasPlaywright,\n hasEslint,\n hasPrettier,\n hasBiome,\n hasEdgeRuntime,\n hasVectorDB,\n hasDrizzle,\n hasPrisma,\n authProvider,\n testing,\n packageManager,\n dependencies,\n configFiles,\n }\n}\n\n/**\n * Analyze dependencies with installed version info\n */\nasync function analyzeDependencies(\n packageJson: any,\n projectPath: string\n): Promise<DependencyInfo[]> {\n const deps: DependencyInfo[] = []\n\n // Production dependencies\n if (packageJson.dependencies) {\n for (const [name, version] of Object.entries(packageJson.dependencies)) {\n const info: DependencyInfo = {\n name,\n version: version as string,\n type: 'dependency',\n }\n\n // Try to get installed version\n try {\n const installedPkgPath = join(projectPath, 'node_modules', name, 'package.json')\n if (existsSync(installedPkgPath)) {\n const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf-8'))\n info.current = installedPkg.version\n }\n } catch {\n // Ignore\n }\n\n deps.push(info)\n }\n }\n\n // Development dependencies\n if (packageJson.devDependencies) {\n for (const [name, version] of Object.entries(packageJson.devDependencies)) {\n const info: DependencyInfo = {\n name,\n version: version as string,\n type: 'devDependency',\n }\n\n try {\n const installedPkgPath = join(projectPath, 'node_modules', name, 'package.json')\n if (existsSync(installedPkgPath)) {\n const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf-8'))\n info.current = installedPkg.version\n }\n } catch {\n // Ignore\n }\n\n deps.push(info)\n }\n }\n\n // Peer dependencies\n if (packageJson.peerDependencies) {\n for (const [name, version] of Object.entries(packageJson.peerDependencies)) {\n deps.push({\n name,\n version: version as string,\n type: 'peerDependency',\n })\n }\n }\n\n return deps\n}\n\n/**\n * Find configuration files in the project\n */\nfunction findConfigFiles(projectPath: string): string[] {\n const configFiles: string[] = []\n\n const commonConfigFiles = [\n // TypeScript\n 'tsconfig.json',\n 'tsconfig.build.json',\n 'tsconfig.test.json',\n\n // Next.js\n 'next.config.js',\n 'next.config.ts',\n 'next.config.mjs',\n 'next-env.d.ts',\n\n // Tailwind\n 'tailwind.config.js',\n 'tailwind.config.ts',\n 'tailwind.config.mjs',\n 'postcss.config.js',\n 'postcss.config.mjs',\n\n // Linting\n '.eslintrc.js',\n '.eslintrc.json',\n '.eslintrc.yml',\n '.eslintrc.yaml',\n 'eslint.config.js',\n 'eslint.config.mjs',\n '.prettierrc',\n '.prettierrc.js',\n '.prettierrc.json',\n 'biome.json',\n 'biome.jsonc',\n\n // Testing\n 'vitest.config.ts',\n 'vitest.config.js',\n 'vitest.config.mts',\n 'jest.config.js',\n 'jest.config.ts',\n 'playwright.config.ts',\n 'cypress.config.js',\n 'cypress.config.ts',\n\n // Build tools\n 'vite.config.ts',\n 'vite.config.js',\n 'webpack.config.js',\n 'rollup.config.js',\n 'turbo.json',\n\n // Database\n 'drizzle.config.ts',\n 'drizzle.config.js',\n 'prisma/schema.prisma',\n\n // Environment\n '.env',\n '.env.local',\n '.env.example',\n '.env.development',\n '.env.production',\n\n // Other\n '.gitignore',\n 'README.md',\n 'package.json',\n 'pnpm-workspace.yaml',\n 'vercel.json',\n 'netlify.toml',\n ]\n\n commonConfigFiles.forEach((file) => {\n if (existsSync(join(projectPath, file))) {\n configFiles.push(file)\n }\n })\n\n // Check for Supabase config\n if (existsSync(join(projectPath, 'supabase', 'config.toml'))) {\n configFiles.push('supabase/config.toml')\n }\n\n return configFiles\n}\n\n/**\n * Check if TypeScript is supported\n */\nfunction hasTypeScriptSupport(projectPath: string, dependencies: DependencyInfo[]): boolean {\n return existsSync(join(projectPath, 'tsconfig.json')) || hasDependency(dependencies, 'typescript')\n}\n\n/**\n * Check if a dependency exists\n */\nfunction hasDependency(dependencies: DependencyInfo[], name: string): boolean {\n return dependencies.some((dep) => dep.name === name)\n}\n\n/**\n * Detect AI SDK support\n */\nfunction detectAISupport(dependencies: DependencyInfo[]): boolean {\n const aiDeps = [\n 'ai',\n '@ai-sdk/openai',\n '@ai-sdk/anthropic',\n '@ai-sdk/google',\n '@ai-sdk/azure',\n '@ai-sdk/mistral',\n '@ai-sdk/cohere',\n 'openai',\n '@anthropic-ai/sdk',\n '@google/generative-ai',\n 'langchain',\n '@langchain/core',\n 'llamaindex',\n ]\n\n return aiDeps.some((dep) => hasDependency(dependencies, dep))\n}\n\n/**\n * Detect Supabase usage\n */\nfunction detectSupabase(dependencies: DependencyInfo[], projectPath: string): boolean {\n const hasSupabaseDeps =\n hasDependency(dependencies, '@supabase/supabase-js') ||\n hasDependency(dependencies, '@supabase/ssr') ||\n hasDependency(dependencies, '@supabase/auth-helpers-nextjs')\n\n const hasSupabaseConfig = existsSync(join(projectPath, 'supabase', 'config.toml'))\n\n return hasSupabaseDeps || hasSupabaseConfig\n}\n\n/**\n * Detect Edge runtime usage\n */\nfunction detectEdgeRuntime(projectPath: string): boolean {\n const middlewarePaths = [\n join(projectPath, 'middleware.ts'),\n join(projectPath, 'middleware.js'),\n join(projectPath, 'src/middleware.ts'),\n join(projectPath, 'src/middleware.js'),\n ]\n\n if (middlewarePaths.some((p) => existsSync(p))) {\n return true\n }\n\n // Check for edge runtime in API routes\n const apiPaths = [\n join(projectPath, 'app/api'),\n join(projectPath, 'src/app/api'),\n join(projectPath, 'pages/api'),\n ]\n\n for (const apiPath of apiPaths) {\n if (existsSync(apiPath)) {\n try {\n const files = getAllFiles(apiPath, ['.ts', '.js'])\n for (const file of files) {\n const content = readFileSync(file, 'utf8')\n if (content.includes(\"export const runtime = 'edge'\")) {\n return true\n }\n }\n } catch {\n // Ignore\n }\n }\n }\n\n return false\n}\n\n/**\n * Check for Supabase vector configuration\n */\nfunction hasSupabaseVectorConfig(projectPath: string): boolean {\n const supabaseMigrationDir = join(projectPath, 'supabase', 'migrations')\n if (!existsSync(supabaseMigrationDir)) return false\n\n try {\n const migrationFiles = readdirSync(supabaseMigrationDir)\n\n return migrationFiles.some((file: string) => {\n if (file.endsWith('.sql')) {\n const content = readFileSync(join(supabaseMigrationDir, file), 'utf8')\n return content.includes('vector') || content.includes('embedding') || content.includes('pgvector')\n }\n return false\n })\n } catch {\n return false\n }\n}\n\n/**\n * Detect Drizzle ORM\n */\nfunction detectDrizzle(dependencies: DependencyInfo[], projectPath: string): boolean {\n const hasDrizzleDeps =\n hasDependency(dependencies, 'drizzle-orm') || hasDependency(dependencies, 'drizzle-kit')\n\n const hasDrizzleConfig =\n existsSync(join(projectPath, 'drizzle.config.ts')) ||\n existsSync(join(projectPath, 'drizzle.config.js'))\n\n return hasDrizzleDeps || hasDrizzleConfig\n}\n\n/**\n * Detect Prisma ORM\n */\nfunction detectPrisma(dependencies: DependencyInfo[], projectPath: string): boolean {\n const hasPrismaDeps =\n hasDependency(dependencies, 'prisma') || hasDependency(dependencies, '@prisma/client')\n\n const hasPrismaSchema = existsSync(join(projectPath, 'prisma', 'schema.prisma'))\n\n return hasPrismaDeps || hasPrismaSchema\n}\n\n/**\n * Detect auth provider\n */\nfunction detectAuthProvider(dependencies: DependencyInfo[], projectPath: string): AuthProvider {\n // Supabase Auth\n if (\n hasDependency(dependencies, '@supabase/auth-helpers-nextjs') ||\n hasDependency(dependencies, '@supabase/ssr')\n ) {\n const hasAuthConfig =\n existsSync(join(projectPath, 'src/lib/supabase')) ||\n existsSync(join(projectPath, 'lib/supabase'))\n if (hasAuthConfig) return 'supabase'\n }\n\n // Clerk\n if (\n hasDependency(dependencies, '@clerk/nextjs') ||\n hasDependency(dependencies, '@clerk/clerk-react')\n ) {\n return 'clerk'\n }\n\n // Better Auth\n if (hasDependency(dependencies, 'better-auth')) {\n return 'better-auth'\n }\n\n // NextAuth / Auth.js\n if (hasDependency(dependencies, 'next-auth') || hasDependency(dependencies, '@auth/core')) {\n return 'next-auth'\n }\n\n // Lucia\n if (hasDependency(dependencies, 'lucia')) {\n return 'lucia'\n }\n\n return 'none'\n}\n\n/**\n * Detect tRPC\n */\nfunction detectTRPC(dependencies: DependencyInfo[]): boolean {\n return (\n hasDependency(dependencies, '@trpc/server') ||\n hasDependency(dependencies, '@trpc/client') ||\n hasDependency(dependencies, '@trpc/react-query')\n )\n}\n\n/**\n * Detect PWA support\n */\nfunction detectPWA(dependencies: DependencyInfo[], projectPath: string): boolean {\n const hasPWADeps =\n hasDependency(dependencies, '@ducanh2912/next-pwa') ||\n hasDependency(dependencies, 'next-pwa') ||\n hasDependency(dependencies, 'workbox-webpack-plugin')\n\n const hasManifest = existsSync(join(projectPath, 'public', 'manifest.json'))\n const hasServiceWorker =\n existsSync(join(projectPath, 'public', 'sw.js')) ||\n existsSync(join(projectPath, 'public', 'service-worker.js'))\n\n return hasPWADeps || (hasManifest && hasServiceWorker)\n}\n\n/**\n * Detect i18n support\n */\nfunction detectI18n(dependencies: DependencyInfo[], projectPath: string): boolean {\n const hasI18nDeps =\n hasDependency(dependencies, 'next-intl') ||\n hasDependency(dependencies, 'next-i18next') ||\n hasDependency(dependencies, 'react-i18next') ||\n hasDependency(dependencies, 'i18next')\n\n const hasMessagesDir =\n existsSync(join(projectPath, 'messages')) ||\n existsSync(join(projectPath, 'locales')) ||\n existsSync(join(projectPath, 'public/locales'))\n\n return hasI18nDeps || hasMessagesDir\n}\n\n/**\n * Detect testing setup\n */\nfunction detectTesting(dependencies: DependencyInfo[]): TestingInfo {\n // Unit testing\n let unit: TestingInfo['unit'] = 'none'\n if (hasDependency(dependencies, 'vitest')) {\n unit = 'vitest'\n } else if (hasDependency(dependencies, 'jest')) {\n unit = 'jest'\n }\n\n // E2E testing\n let e2e: TestingInfo['e2e'] = 'none'\n if (hasDependency(dependencies, '@playwright/test') || hasDependency(dependencies, 'playwright')) {\n e2e = 'playwright'\n } else if (hasDependency(dependencies, 'cypress')) {\n e2e = 'cypress'\n }\n\n return { unit, e2e }\n}\n\n/**\n * Get all files in directory recursively\n */\nfunction getAllFiles(dirPath: string, extensions: string[]): string[] {\n const files: string[] = []\n\n try {\n const entries = readdirSync(dirPath, { withFileTypes: true })\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name)\n if (entry.isDirectory()) {\n files.push(...getAllFiles(fullPath, extensions))\n } else if (extensions.some((ext) => entry.name.endsWith(ext))) {\n files.push(fullPath)\n }\n }\n } catch {\n // Ignore errors\n }\n\n return files\n}\n\n/**\n * Check what AT3 features are missing from the project\n */\nexport function getMissingFeatures(info: ProjectInfo): string[] {\n const missing: string[] = []\n\n if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) missing.push('database')\n if (!info.hasAI) missing.push('ai')\n if (!info.hasPWA) missing.push('pwa')\n if (!info.hasI18n) missing.push('i18n')\n if (info.testing.unit === 'none') missing.push('testing')\n if (!info.hasTailwind) missing.push('tailwind')\n if (!info.hasTypeScript) missing.push('typescript')\n if (info.authProvider === 'none') missing.push('auth')\n\n return missing\n}\n\n/**\n * Check if project is compatible with AT3 stack\n */\nexport function isCompatible(info: ProjectInfo): boolean {\n // Must have Next.js or React\n if (!(info.hasNextjs || info.hasReact)) return false\n\n // Must have a supported package manager\n if (!['npm', 'pnpm', 'yarn', 'bun'].includes(info.packageManager)) return false\n\n return true\n}\n\n/**\n * Get upgrade recommendations based on current project state\n */\nexport function getRecommendations(info: ProjectInfo): {\n priority: 'high' | 'medium' | 'low'\n feature: string\n reason: string\n}[] {\n const recommendations = []\n\n if (!info.hasTypeScript) {\n recommendations.push({\n priority: 'high' as const,\n feature: 'typescript',\n reason: 'TypeScript provides better development experience and type safety',\n })\n }\n\n if (!info.hasTailwind) {\n recommendations.push({\n priority: 'high' as const,\n feature: 'tailwind',\n reason: 'Tailwind CSS is essential for AT3 stack styling',\n })\n }\n\n if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) {\n recommendations.push({\n priority: 'high' as const,\n feature: 'database',\n reason: 'A database solution is needed for most applications',\n })\n }\n\n if (info.authProvider === 'none') {\n recommendations.push({\n priority: 'medium' as const,\n feature: 'auth',\n reason: 'Authentication is essential for user management',\n })\n }\n\n if (!info.hasAI) {\n recommendations.push({\n priority: 'medium' as const,\n feature: 'ai',\n reason: 'AI integration is a core feature of AT3 stack',\n })\n }\n\n if (info.testing.unit === 'none') {\n recommendations.push({\n priority: 'low' as const,\n feature: 'testing',\n reason: 'Comprehensive testing improves code quality',\n })\n }\n\n if (!info.hasBiome && (info.hasEslint || info.hasPrettier)) {\n recommendations.push({\n priority: 'low' as const,\n feature: 'biome',\n reason: 'Biome provides faster linting and formatting than ESLint/Prettier',\n })\n }\n\n return recommendations\n}\n\n/**\n * Get a summary score for AT3 stack compatibility\n */\nexport function getAT3Score(info: ProjectInfo): {\n score: number\n maxScore: number\n percentage: number\n level: 'none' | 'basic' | 'intermediate' | 'advanced' | 'full'\n} {\n let score = 0\n const maxScore = 10\n\n if (info.hasNextjs) score += 1\n if (info.hasTypeScript) score += 1\n if (info.hasTailwind) score += 1\n if (info.hasSupabase || info.hasDrizzle || info.hasPrisma) score += 1\n if (info.authProvider !== 'none') score += 1\n if (info.hasAI) score += 2\n if (info.hasEdgeRuntime) score += 1\n if (info.testing.unit !== 'none') score += 1\n if (info.hasBiome) score += 1\n\n const percentage = Math.round((score / maxScore) * 100)\n\n let level: 'none' | 'basic' | 'intermediate' | 'advanced' | 'full'\n if (percentage === 0) level = 'none'\n else if (percentage < 30) level = 'basic'\n else if (percentage < 60) level = 'intermediate'\n else if (percentage < 90) level = 'advanced'\n else level = 'full'\n\n return { score, maxScore, percentage, level }\n}\n","/**\n * Add AI integration to existing project\n */\n\nimport { join } from 'node:path'\nimport { ensureDir, pathExists, readFile, writeFile } from 'fs-extra'\n\nexport async function addAI(type: 'custom' | 'vercel', projectPath: string): Promise<void> {\n const srcPath = join(projectPath, 'src')\n await ensureDir(srcPath)\n\n // Create lib/ai directory\n const aiPath = join(srcPath, 'lib', 'ai')\n await ensureDir(aiPath)\n\n if (type === 'custom') {\n await addCustomAI(aiPath)\n }\n\n if (type === 'vercel') {\n await addVercelAI(aiPath)\n }\n\n // Create API routes\n const apiPath = join(srcPath, 'app', 'api')\n await ensureDir(apiPath)\n\n await addAPIRoutes(apiPath, type)\n\n // Update package.json\n await updatePackageJson(projectPath, type)\n}\n\nasync function addCustomAI(aiPath: string): Promise<void> {\n // AI client configuration\n const clientConfig = `/**\n * Custom AI client configuration\n */\n\nexport interface AIProvider {\n name: string;\n baseURL: string;\n apiKey: string;\n}\n\nexport const AI_PROVIDERS = {\n openai: {\n name: 'OpenAI',\n baseURL: 'https://api.openai.com/v1',\n apiKey: process.env.OPENAI_API_KEY!,\n },\n anthropic: {\n name: 'Anthropic',\n baseURL: 'https://api.anthropic.com',\n apiKey: process.env.ANTHROPIC_API_KEY!,\n },\n google: {\n name: 'Google AI',\n baseURL: 'https://generativelanguage.googleapis.com',\n apiKey: process.env.GOOGLE_AI_API_KEY!,\n },\n} as const;\n\nexport type AIProviderKey = keyof typeof AI_PROVIDERS;\n`\n\n await writeFile(join(aiPath, 'config.ts'), clientConfig)\n\n // AI client\n const client = `/**\n * Custom AI client implementation\n */\n\nimport { AI_PROVIDERS, type AIProviderKey } from './config.js';\n\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface CompletionOptions {\n model: string;\n temperature?: number;\n maxTokens?: number;\n}\n\nexport class AIClient {\n constructor(\n private provider: AIProviderKey,\n private options: CompletionOptions = {}\n ) {}\n\n async completion(\n messages: ChatMessage[],\n options: Partial<CompletionOptions> = {}\n ): Promise<string> {\n const config = AI_PROVIDERS[this.provider];\n const finalOptions = { ...this.options, ...options };\n\n // Implementation would depend on the provider\n // This is a basic structure\n const response = await fetch(\\`\\${config.baseURL}/chat/completions\\`, {\n method: 'POST',\n headers: {\n 'Authorization': \\`Bearer \\${config.apiKey}\\`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n model: finalOptions.model || 'gpt-3.5-turbo',\n messages,\n temperature: finalOptions.temperature || 0.7,\n max_tokens: finalOptions.maxTokens || 1000,\n }),\n });\n\n const data = await response.json();\n return data.choices[0]?.message?.content || '';\n }\n}\n`\n\n await writeFile(join(aiPath, 'client.ts'), client)\n}\n\nasync function addVercelAI(aiPath: string): Promise<void> {\n // Vercel AI client\n const vercelClient = `/**\n * Vercel AI SDK integration\n */\n\nimport { anthropic } from '@ai-sdk/anthropic';\nimport { google } from '@ai-sdk/google';\nimport { openai } from '@ai-sdk/openai';\nimport { generateText, streamText } from 'ai';\n\nexport const AI_MODELS = {\n 'gpt-4-turbo': openai('gpt-4-turbo'),\n 'gpt-3.5-turbo': openai('gpt-3.5-turbo'),\n 'claude-3-haiku': anthropic('claude-3-haiku-20240307'),\n 'claude-3-sonnet': anthropic('claude-3-sonnet-20240229'),\n 'gemini-pro': google('models/gemini-pro'),\n} as const;\n\nexport type AIModelKey = keyof typeof AI_MODELS;\n\nexport async function generateCompletion(\n model: AIModelKey,\n prompt: string,\n options?: {\n temperature?: number;\n maxTokens?: number;\n }\n) {\n const { text } = await generateText({\n model: AI_MODELS[model],\n prompt,\n temperature: options?.temperature || 0.7,\n maxTokens: options?.maxTokens || 1000,\n });\n\n return text;\n}\n\nexport async function streamCompletion(\n model: AIModelKey,\n prompt: string,\n options?: {\n temperature?: number;\n maxTokens?: number;\n }\n) {\n const { textStream } = await streamText({\n model: AI_MODELS[model],\n prompt,\n temperature: options?.temperature || 0.7,\n maxTokens: options?.maxTokens || 1000,\n });\n\n return textStream;\n}\n`\n\n await writeFile(join(aiPath, 'vercel-client.ts'), vercelClient)\n\n // Vercel AI hooks\n const hooks = `/**\n * React hooks for Vercel AI SDK\n */\n\n'use client';\n\nimport { useChat, useCompletion } from 'ai/react';\nimport { type AIModelKey } from './vercel-client.js';\n\nexport function useAIChat(model: AIModelKey = 'gpt-3.5-turbo') {\n return useChat({\n api: '/api/chat',\n body: {\n model,\n },\n });\n}\n\nexport function useAICompletion(model: AIModelKey = 'gpt-3.5-turbo') {\n return useCompletion({\n api: '/api/completion',\n body: {\n model,\n },\n });\n}\n`\n\n await writeFile(join(aiPath, 'vercel-hooks.ts'), hooks)\n}\n\nasync function addAPIRoutes(apiPath: string, type: 'custom' | 'vercel'): Promise<void> {\n if (type === 'vercel') {\n // Chat API route\n const chatRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client';\nimport { streamText } from 'ai';\n\nexport async function POST(req: Request) {\n const { messages, model = 'gpt-3.5-turbo' } = await req.json();\n\n const result = await streamText({\n model: AI_MODELS[model as AIModelKey],\n messages,\n });\n\n return result.toDataStreamResponse();\n}\n`\n\n await ensureDir(join(apiPath, 'chat'))\n await writeFile(join(apiPath, 'chat', 'route.ts'), chatRoute)\n\n // Completion API route\n const completionRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client';\nimport { generateText } from 'ai';\n\nexport async function POST(req: Request) {\n const { prompt, model = 'gpt-3.5-turbo' } = await req.json();\n\n const { text } = await generateText({\n model: AI_MODELS[model as AIModelKey],\n prompt,\n });\n\n return Response.json({ text });\n}\n`\n\n await ensureDir(join(apiPath, 'completion'))\n await writeFile(join(apiPath, 'completion', 'route.ts'), completionRoute)\n }\n}\n\nasync function updatePackageJson(projectPath: string, type: 'custom' | 'vercel'): Promise<void> {\n const packageJsonPath = join(projectPath, 'package.json')\n\n if (!(await pathExists(packageJsonPath))) return\n\n const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))\n\n if (!packageJson.dependencies) packageJson.dependencies = {}\n\n if (type === 'vercel') {\n packageJson.dependencies.ai = '^5.0.104'\n packageJson.dependencies['@ai-sdk/openai'] = '^2.0.74'\n packageJson.dependencies['@ai-sdk/anthropic'] = '^2.0.50'\n packageJson.dependencies['@ai-sdk/google'] = '^2.0.44'\n }\n\n await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))\n}\n","\nimport { readFile, writeFile, pathExists, ensureDir } from 'fs-extra'\nimport { join } from 'path'\nimport { colors } from '../utils/cli-styling'\n\nexport async function addBetterAuth(projectPath: string): Promise<void> {\n console.log(colors.info('Adding Better Auth...'))\n\n // 1. Update package.json\n await updatePackageJson(projectPath)\n\n // 2. Add env vars\n await addEnvExample(projectPath)\n\n // 3. Create auth client/server files\n await createAuthFiles(projectPath)\n\n console.log(colors.success('✓ Added Better Auth configuration'))\n}\n\nasync function updatePackageJson(projectPath: string): Promise<void> {\n const packageJsonPath = join(projectPath, 'package.json')\n if (!(await pathExists(packageJsonPath))) return\n\n const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))\n\n if (!packageJson.dependencies) packageJson.dependencies = {}\n packageJson.dependencies['better-auth'] = '^1.1.0' // Check version\n\n await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))\n}\n\nasync function addEnvExample(projectPath: string): Promise<void> {\n const envExamplePath = join(projectPath, '.env.example')\n let envContent = ''\n\n if (await pathExists(envExamplePath)) {\n envContent = await readFile(envExamplePath, 'utf-8')\n }\n\n if (!envContent.includes('BETTER_AUTH_SECRET')) {\n envContent += `\n# Better Auth\nBETTER_AUTH_SECRET=your_secret_here\nBETTER_AUTH_URL=http://localhost:3000\n`\n await writeFile(envExamplePath, envContent)\n }\n}\n\nasync function createAuthFiles(projectPath: string): Promise<void> {\n const libPath = join(projectPath, 'src', 'lib', 'auth')\n await ensureDir(libPath)\n\n // client.ts\n await writeFile(join(libPath, 'client.ts'), `import { createAuthClient } from \"better-auth/react\"\nexport const authClient = createAuthClient({\n baseURL: process.env.BETTER_AUTH_URL\n})\n`)\n\n // server.ts (Basic stub, assumes Drizzle or similar will be connected manually for now)\n await writeFile(join(libPath, 'auth.ts'), `import { betterAuth } from \"better-auth\";\n// import { db } from \"@/db\";\n// import { drizzleAdapter } from \"better-auth/adapters/drizzle\";\n\nexport const auth = betterAuth({\n // adapter: drizzleAdapter(db, {\n // provider: \"pg\",\n // }),\n emailAndPassword: {\n enabled: true\n },\n // socialProviders: {\n // github: {\n // clientId: process.env.GITHUB_CLIENT_ID,\n // clientSecret: process.env.GITHUB_CLIENT_SECRET,\n // }\n // },\n})\n`)\n\n // API Route\n const apiPath = join(projectPath, 'src', 'app', 'api', 'auth', '[...all]')\n await ensureDir(apiPath)\n\n await writeFile(join(apiPath, 'route.ts'), `import { auth } from \"@/lib/auth/auth\";\nimport { toNextJsHandler } from \"better-auth/next-js\";\n\nexport const { GET, POST } = toNextJsHandler(auth);\n`)\n}\n","/**\n * CLI styling utilities for AT3 Stack Kit\n * Aligned with at3-toolkit's sophisticated styling system\n */\n\nimport boxen from 'boxen'\nimport chalk from 'chalk'\nimport figures from 'figures'\nimport gradient from 'gradient-string'\n\n// Color palette\nexport const colors = {\n primary: chalk.hex('#2563eb'), // Blue\n secondary: chalk.hex('#7c3aed'), // Purple\n success: chalk.green,\n error: chalk.red,\n warning: chalk.yellow,\n info: chalk.blue,\n muted: chalk.gray,\n accent: chalk.magenta,\n ai: chalk.hex('#10b981'), // Emerald for AI features\n} as const\n\n// Gradients\nexport const gradients = {\n at3: gradient('#2563eb', '#10b981'), // Blue to Emerald\n ai: gradient('#10b981', '#059669'), // Emerald gradient\n success: gradient('#22c55e', '#16a34a'), // Green gradient\n warning: gradient('#f59e0b', '#d97706'), // Amber gradient\n error: gradient('#ef4444', '#dc2626'), // Red gradient\n} as const\n\n// Icons and symbols\nexport const symbols = {\n success: colors.success(figures.tick),\n error: colors.error(figures.cross),\n warning: colors.warning(figures.warning),\n info: colors.info(figures.info),\n arrow: colors.muted(figures.arrowRight),\n bullet: colors.muted(figures.bullet),\n line: colors.muted(figures.line),\n ai: colors.ai('🤖'),\n stack: colors.primary('📚'),\n edge: colors.secondary('⚡'),\n database: colors.info('🗄️'),\n config: colors.muted('⚙️'),\n} as const\n\n// Text styling utilities\nexport const style = {\n title: (text: string) => gradients.at3(text),\n subtitle: (text: string) => colors.secondary(text),\n heading: (text: string) => colors.primary.bold(text),\n label: (text: string) => colors.muted(text),\n value: (text: string) => colors.primary(text),\n success: (text: string) => colors.success(text),\n error: (text: string) => colors.error(text),\n warning: (text: string) => colors.warning(text),\n info: (text: string) => colors.info(text),\n muted: (text: string) => colors.muted(text),\n accent: (text: string) => colors.accent(text),\n ai: (text: string) => colors.ai(text),\n code: (text: string) => chalk.cyan.italic(text),\n path: (text: string) => chalk.dim.underline(text),\n command: (text: string) => chalk.bgBlack.white.bold(` ${text} `),\n} as const\n\n// Box utilities\nexport const box = {\n info: (content: string, title?: string) =>\n boxen(content, {\n ...(title && { title }),\n titleAlignment: 'center',\n padding: 1,\n margin: 1,\n borderStyle: 'round',\n borderColor: 'blue',\n }),\n success: (content: string, title?: string) =>\n boxen(content, {\n ...(title && { title }),\n titleAlignment: 'center',\n padding: 1,\n margin: 1,\n borderStyle: 'round',\n borderColor: 'green',\n }),\n warning: (content: string, title?: string) =>\n boxen(content, {\n ...(title && { title }),\n titleAlignment: 'center',\n padding: 1,\n margin: 1,\n borderStyle: 'round',\n borderColor: 'yellow',\n }),\n error: (content: string, title?: string) =>\n boxen(content, {\n ...(title && { title }),\n titleAlignment: 'center',\n padding: 1,\n margin: 1,\n borderStyle: 'round',\n borderColor: 'red',\n }),\n} as const\n\n// Progress indicators\nexport const progress = {\n step: (current: number, total: number, description: string) =>\n `${colors.muted(`[${current}/${total}]`)} ${description}`,\n\n percentage: (percent: number) => {\n const filled = Math.floor(percent / 5)\n const empty = 20 - filled\n const bar = '█'.repeat(filled) + '░'.repeat(empty)\n return `${colors.primary(bar)} ${colors.muted(`${percent}%`)}`\n },\n}\n\n// Header utilities\nexport const header = {\n main: (title: string, subtitle?: string) => {\n const titleText = gradients.at3.multiline(title)\n const subtitleText = subtitle ? `\\n${colors.muted(subtitle)}` : ''\n return `${titleText}${subtitleText}`\n },\n\n section: (title: string) =>\n `\\n${colors.primary.bold(title)}\\n${colors.muted('─'.repeat(title.length))}`,\n}\n\n// List utilities\nexport const list = {\n item: (text: string, symbol = symbols.bullet) => `${symbol} ${text}`,\n checkItem: (text: string, checked = false) => {\n const icon = checked ? symbols.success : colors.muted(figures.radioOff)\n return `${icon} ${text}`\n },\n numbered: (items: string[]) =>\n items.map((item, i) => `${colors.muted(`${i + 1}.`)} ${item}`).join('\\n'),\n}\n","\nimport { readFile, writeFile, pathExists } from 'fs-extra'\nimport { join } from 'path'\nimport { colors } from '../utils/cli-styling'\n\nexport async function addClerk(projectPath: string): Promise<void> {\n console.log(colors.info('Adding Clerk authentication...'))\n\n // 1. Install dependencies\n // This will be handled by the main CLI loop if we return dependencies,\n // but for now we assume the caller handles installation or we update package.json directly\n await updatePackageJson(projectPath)\n\n // 2. Add environment variables\n await addEnvExample(projectPath)\n\n // 3. Add Middleware\n await addMiddleware(projectPath)\n\n // 4. Wrap Layout (This is tricky with regex, might need manual instruction or smart AST)\n // For now, we'll log instructions for the layout part as it's high risk to break\n console.log(colors.warning('NOTE: You need to wrap your root layout with <ClerkProvider> manually.'))\n\n console.log(colors.success('✓ Added Clerk configuration'))\n}\n\nasync function updatePackageJson(projectPath: string): Promise<void> {\n const packageJsonPath = join(projectPath, 'package.json')\n if (!(await pathExists(packageJsonPath))) return\n\n const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))\n\n if (!packageJson.dependencies) packageJson.dependencies = {}\n packageJson.dependencies['@clerk/nextjs'] = '^6.9.0'\n\n await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2))\n}\n\nasync function addEnvExample(projectPath: string): Promise<void> {\n const envExamplePath = join(projectPath, '.env.example')\n let envContent = ''\n\n if (await pathExists(envExamplePath)) {\n envContent = await readFile(envExamplePath, 'u