UNPKG

@entro314labs/at3-stack-kit

Version:

Upgrade existing projects to AT3 Stack with intelligent migration

1,575 lines (1,490 loc) 113 kB
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) { if (typeof require !== "undefined") return require.apply(this, arguments); throw Error('Dynamic require of "' + x + '" is not supported'); }); // src/cli.ts import { cancel, confirm, intro, isCancel, multiselect, note, outro, spinner } from "@clack/prompts"; import chalk2 from "chalk"; import { program } from "commander"; import { detect as detectPackageManager2 } from "detect-package-manager"; import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs-extra"; import { join as join11 } from "path"; // src/detect.ts import { detect as detectPackageManager } from "detect-package-manager"; import { existsSync, readdirSync, readFileSync } from "fs-extra"; import { join } from "path"; async function detectProjectType(projectPath) { const packageJsonPath = join(projectPath, "package.json"); if (!existsSync(packageJsonPath)) { return "unknown"; } try { const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const hasAI = deps.ai || deps["@ai-sdk/openai"] || deps["@ai-sdk/anthropic"] || deps["@ai-sdk/google"]; const hasSupabase = deps["@supabase/supabase-js"] || deps["@supabase/ssr"]; const hasNextJS = deps.next; const hasTailwind = deps.tailwindcss; const hasTypeScript = deps.typescript || existsSync(join(projectPath, "tsconfig.json")); if (hasAI && hasSupabase && hasNextJS && (hasTailwind || hasTypeScript)) { return "ait3e"; } if (deps.next && deps["@trpc/server"]) { if (deps.prisma || deps["@prisma/client"] || deps["drizzle-orm"]) { return "t3"; } } if (deps.next) return "nextjs"; if (deps.nuxt || deps["@nuxt/core"]) return "nuxt"; if (deps.vue) return "vue"; if (deps.react) return "react"; if (deps.vite) return "vite"; if (deps.webpack) return "webpack"; return "node"; } catch (error) { return "unknown"; } } async function analyzeProject(projectPath) { if (!existsSync(projectPath)) { throw new Error(`Project path does not exist: ${projectPath}`); } const packageJsonPath = join(projectPath, "package.json"); if (!existsSync(packageJsonPath)) { throw new Error("No package.json found. This does not appear to be a Node.js project."); } const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); let packageManager = "npm"; try { packageManager = await detectPackageManager({ cwd: projectPath }); } catch { if (existsSync(join(projectPath, "pnpm-lock.yaml"))) packageManager = "pnpm"; else if (existsSync(join(projectPath, "yarn.lock"))) packageManager = "yarn"; else if (existsSync(join(projectPath, "bun.lockb"))) packageManager = "bun"; } const dependencies = await analyzeDependencies(packageJson, projectPath); const configFiles = findConfigFiles(projectPath); const hasTypeScript = hasTypeScriptSupport(projectPath, dependencies); const hasNextjs = hasDependency(dependencies, "next"); const hasReact = hasDependency(dependencies, "react"); const hasVue = hasDependency(dependencies, "vue"); const hasTailwind = hasDependency(dependencies, "tailwindcss"); const hasTRPC = detectTRPC(dependencies); const hasEslint = hasDependency(dependencies, "eslint"); const hasPrettier = hasDependency(dependencies, "prettier"); const hasBiome = hasDependency(dependencies, "@biomejs/biome"); const hasAI = detectAISupport(dependencies); const hasSupabase = detectSupabase(dependencies, projectPath); const hasEdgeRuntime = detectEdgeRuntime(projectPath); const hasVectorDB = hasSupabaseVectorConfig(projectPath); const hasPWA = detectPWA(dependencies, projectPath); const hasI18n = detectI18n(dependencies, projectPath); const hasDrizzle = detectDrizzle(dependencies, projectPath); const hasPrisma = detectPrisma(dependencies, projectPath); const authProvider = detectAuthProvider(dependencies, projectPath); const testing = detectTesting(dependencies); const hasVitest = testing.unit === "vitest"; const hasPlaywright = testing.e2e === "playwright"; return { path: projectPath, type: await detectProjectType(projectPath), hasNextjs, hasReact, hasVue, hasTypeScript, hasTailwind, hasTRPC, hasSupabase, hasAI, hasPWA, hasI18n, hasVitest, hasPlaywright, hasEslint, hasPrettier, hasBiome, hasEdgeRuntime, hasVectorDB, hasDrizzle, hasPrisma, authProvider, testing, packageManager, dependencies, configFiles }; } async function analyzeDependencies(packageJson, projectPath) { const deps = []; if (packageJson.dependencies) { for (const [name, version] of Object.entries(packageJson.dependencies)) { const info = { name, version, type: "dependency" }; try { const installedPkgPath = join(projectPath, "node_modules", name, "package.json"); if (existsSync(installedPkgPath)) { const installedPkg = JSON.parse(readFileSync(installedPkgPath, "utf-8")); info.current = installedPkg.version; } } catch { } deps.push(info); } } if (packageJson.devDependencies) { for (const [name, version] of Object.entries(packageJson.devDependencies)) { const info = { name, version, type: "devDependency" }; try { const installedPkgPath = join(projectPath, "node_modules", name, "package.json"); if (existsSync(installedPkgPath)) { const installedPkg = JSON.parse(readFileSync(installedPkgPath, "utf-8")); info.current = installedPkg.version; } } catch { } deps.push(info); } } if (packageJson.peerDependencies) { for (const [name, version] of Object.entries(packageJson.peerDependencies)) { deps.push({ name, version, type: "peerDependency" }); } } return deps; } function findConfigFiles(projectPath) { const configFiles = []; const commonConfigFiles = [ // TypeScript "tsconfig.json", "tsconfig.build.json", "tsconfig.test.json", // Next.js "next.config.js", "next.config.ts", "next.config.mjs", "next-env.d.ts", // Tailwind "tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs", "postcss.config.js", "postcss.config.mjs", // Linting ".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", ".eslintrc.yaml", "eslint.config.js", "eslint.config.mjs", ".prettierrc", ".prettierrc.js", ".prettierrc.json", "biome.json", "biome.jsonc", // Testing "vitest.config.ts", "vitest.config.js", "vitest.config.mts", "jest.config.js", "jest.config.ts", "playwright.config.ts", "cypress.config.js", "cypress.config.ts", // Build tools "vite.config.ts", "vite.config.js", "webpack.config.js", "rollup.config.js", "turbo.json", // Database "drizzle.config.ts", "drizzle.config.js", "prisma/schema.prisma", // Environment ".env", ".env.local", ".env.example", ".env.development", ".env.production", // Other ".gitignore", "README.md", "package.json", "pnpm-workspace.yaml", "vercel.json", "netlify.toml" ]; commonConfigFiles.forEach((file) => { if (existsSync(join(projectPath, file))) { configFiles.push(file); } }); if (existsSync(join(projectPath, "supabase", "config.toml"))) { configFiles.push("supabase/config.toml"); } return configFiles; } function hasTypeScriptSupport(projectPath, dependencies) { return existsSync(join(projectPath, "tsconfig.json")) || hasDependency(dependencies, "typescript"); } function hasDependency(dependencies, name) { return dependencies.some((dep) => dep.name === name); } function detectAISupport(dependencies) { const aiDeps = [ "ai", "@ai-sdk/openai", "@ai-sdk/anthropic", "@ai-sdk/google", "@ai-sdk/azure", "@ai-sdk/mistral", "@ai-sdk/cohere", "openai", "@anthropic-ai/sdk", "@google/generative-ai", "langchain", "@langchain/core", "llamaindex" ]; return aiDeps.some((dep) => hasDependency(dependencies, dep)); } function detectSupabase(dependencies, projectPath) { const hasSupabaseDeps = hasDependency(dependencies, "@supabase/supabase-js") || hasDependency(dependencies, "@supabase/ssr") || hasDependency(dependencies, "@supabase/auth-helpers-nextjs"); const hasSupabaseConfig = existsSync(join(projectPath, "supabase", "config.toml")); return hasSupabaseDeps || hasSupabaseConfig; } function detectEdgeRuntime(projectPath) { const middlewarePaths = [ join(projectPath, "middleware.ts"), join(projectPath, "middleware.js"), join(projectPath, "src/middleware.ts"), join(projectPath, "src/middleware.js") ]; if (middlewarePaths.some((p) => existsSync(p))) { return true; } const apiPaths = [ join(projectPath, "app/api"), join(projectPath, "src/app/api"), join(projectPath, "pages/api") ]; for (const apiPath of apiPaths) { if (existsSync(apiPath)) { try { const files = getAllFiles(apiPath, [".ts", ".js"]); for (const file of files) { const content = readFileSync(file, "utf8"); if (content.includes("export const runtime = 'edge'")) { return true; } } } catch { } } } return false; } function hasSupabaseVectorConfig(projectPath) { const supabaseMigrationDir = join(projectPath, "supabase", "migrations"); if (!existsSync(supabaseMigrationDir)) return false; try { const migrationFiles = readdirSync(supabaseMigrationDir); return migrationFiles.some((file) => { if (file.endsWith(".sql")) { const content = readFileSync(join(supabaseMigrationDir, file), "utf8"); return content.includes("vector") || content.includes("embedding") || content.includes("pgvector"); } return false; }); } catch { return false; } } function detectDrizzle(dependencies, projectPath) { const hasDrizzleDeps = hasDependency(dependencies, "drizzle-orm") || hasDependency(dependencies, "drizzle-kit"); const hasDrizzleConfig = existsSync(join(projectPath, "drizzle.config.ts")) || existsSync(join(projectPath, "drizzle.config.js")); return hasDrizzleDeps || hasDrizzleConfig; } function detectPrisma(dependencies, projectPath) { const hasPrismaDeps = hasDependency(dependencies, "prisma") || hasDependency(dependencies, "@prisma/client"); const hasPrismaSchema = existsSync(join(projectPath, "prisma", "schema.prisma")); return hasPrismaDeps || hasPrismaSchema; } function detectAuthProvider(dependencies, projectPath) { if (hasDependency(dependencies, "@supabase/auth-helpers-nextjs") || hasDependency(dependencies, "@supabase/ssr")) { const hasAuthConfig = existsSync(join(projectPath, "src/lib/supabase")) || existsSync(join(projectPath, "lib/supabase")); if (hasAuthConfig) return "supabase"; } if (hasDependency(dependencies, "@clerk/nextjs") || hasDependency(dependencies, "@clerk/clerk-react")) { return "clerk"; } if (hasDependency(dependencies, "better-auth")) { return "better-auth"; } if (hasDependency(dependencies, "next-auth") || hasDependency(dependencies, "@auth/core")) { return "next-auth"; } if (hasDependency(dependencies, "lucia")) { return "lucia"; } return "none"; } function detectTRPC(dependencies) { return hasDependency(dependencies, "@trpc/server") || hasDependency(dependencies, "@trpc/client") || hasDependency(dependencies, "@trpc/react-query"); } function detectPWA(dependencies, projectPath) { const hasPWADeps = hasDependency(dependencies, "@ducanh2912/next-pwa") || hasDependency(dependencies, "next-pwa") || hasDependency(dependencies, "workbox-webpack-plugin"); const hasManifest = existsSync(join(projectPath, "public", "manifest.json")); const hasServiceWorker = existsSync(join(projectPath, "public", "sw.js")) || existsSync(join(projectPath, "public", "service-worker.js")); return hasPWADeps || hasManifest && hasServiceWorker; } function detectI18n(dependencies, projectPath) { const hasI18nDeps = hasDependency(dependencies, "next-intl") || hasDependency(dependencies, "next-i18next") || hasDependency(dependencies, "react-i18next") || hasDependency(dependencies, "i18next"); const hasMessagesDir = existsSync(join(projectPath, "messages")) || existsSync(join(projectPath, "locales")) || existsSync(join(projectPath, "public/locales")); return hasI18nDeps || hasMessagesDir; } function detectTesting(dependencies) { let unit = "none"; if (hasDependency(dependencies, "vitest")) { unit = "vitest"; } else if (hasDependency(dependencies, "jest")) { unit = "jest"; } let e2e = "none"; if (hasDependency(dependencies, "@playwright/test") || hasDependency(dependencies, "playwright")) { e2e = "playwright"; } else if (hasDependency(dependencies, "cypress")) { e2e = "cypress"; } return { unit, e2e }; } function getAllFiles(dirPath, extensions) { const files = []; try { const entries = readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(dirPath, entry.name); if (entry.isDirectory()) { files.push(...getAllFiles(fullPath, extensions)); } else if (extensions.some((ext) => entry.name.endsWith(ext))) { files.push(fullPath); } } } catch { } return files; } function getMissingFeatures(info) { const missing = []; if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) missing.push("database"); if (!info.hasAI) missing.push("ai"); if (!info.hasPWA) missing.push("pwa"); if (!info.hasI18n) missing.push("i18n"); if (info.testing.unit === "none") missing.push("testing"); if (!info.hasTailwind) missing.push("tailwind"); if (!info.hasTypeScript) missing.push("typescript"); if (info.authProvider === "none") missing.push("auth"); return missing; } function isCompatible(info) { if (!(info.hasNextjs || info.hasReact)) return false; if (!["npm", "pnpm", "yarn", "bun"].includes(info.packageManager)) return false; return true; } function getRecommendations(info) { const recommendations = []; if (!info.hasTypeScript) { recommendations.push({ priority: "high", feature: "typescript", reason: "TypeScript provides better development experience and type safety" }); } if (!info.hasTailwind) { recommendations.push({ priority: "high", feature: "tailwind", reason: "Tailwind CSS is essential for AT3 stack styling" }); } if (!info.hasSupabase && !info.hasDrizzle && !info.hasPrisma) { recommendations.push({ priority: "high", feature: "database", reason: "A database solution is needed for most applications" }); } if (info.authProvider === "none") { recommendations.push({ priority: "medium", feature: "auth", reason: "Authentication is essential for user management" }); } if (!info.hasAI) { recommendations.push({ priority: "medium", feature: "ai", reason: "AI integration is a core feature of AT3 stack" }); } if (info.testing.unit === "none") { recommendations.push({ priority: "low", feature: "testing", reason: "Comprehensive testing improves code quality" }); } if (!info.hasBiome && (info.hasEslint || info.hasPrettier)) { recommendations.push({ priority: "low", feature: "biome", reason: "Biome provides faster linting and formatting than ESLint/Prettier" }); } return recommendations; } function getAT3Score(info) { let score = 0; const maxScore = 10; if (info.hasNextjs) score += 1; if (info.hasTypeScript) score += 1; if (info.hasTailwind) score += 1; if (info.hasSupabase || info.hasDrizzle || info.hasPrisma) score += 1; if (info.authProvider !== "none") score += 1; if (info.hasAI) score += 2; if (info.hasEdgeRuntime) score += 1; if (info.testing.unit !== "none") score += 1; if (info.hasBiome) score += 1; const percentage = Math.round(score / maxScore * 100); let level; if (percentage === 0) level = "none"; else if (percentage < 30) level = "basic"; else if (percentage < 60) level = "intermediate"; else if (percentage < 90) level = "advanced"; else level = "full"; return { score, maxScore, percentage, level }; } // src/features/add-ai.ts import { join as join2 } from "path"; import { ensureDir, pathExists, readFile, writeFile } from "fs-extra"; async function addAI(type, projectPath) { const srcPath = join2(projectPath, "src"); await ensureDir(srcPath); const aiPath = join2(srcPath, "lib", "ai"); await ensureDir(aiPath); if (type === "custom") { await addCustomAI(aiPath); } if (type === "vercel") { await addVercelAI(aiPath); } const apiPath = join2(srcPath, "app", "api"); await ensureDir(apiPath); await addAPIRoutes(apiPath, type); await updatePackageJson(projectPath, type); } async function addCustomAI(aiPath) { const clientConfig = `/** * Custom AI client configuration */ export interface AIProvider { name: string; baseURL: string; apiKey: string; } export const AI_PROVIDERS = { openai: { name: 'OpenAI', baseURL: 'https://api.openai.com/v1', apiKey: process.env.OPENAI_API_KEY!, }, anthropic: { name: 'Anthropic', baseURL: 'https://api.anthropic.com', apiKey: process.env.ANTHROPIC_API_KEY!, }, google: { name: 'Google AI', baseURL: 'https://generativelanguage.googleapis.com', apiKey: process.env.GOOGLE_AI_API_KEY!, }, } as const; export type AIProviderKey = keyof typeof AI_PROVIDERS; `; await writeFile(join2(aiPath, "config.ts"), clientConfig); const client = `/** * Custom AI client implementation */ import { AI_PROVIDERS, type AIProviderKey } from './config.js'; export interface ChatMessage { role: 'system' | 'user' | 'assistant'; content: string; } export interface CompletionOptions { model: string; temperature?: number; maxTokens?: number; } export class AIClient { constructor( private provider: AIProviderKey, private options: CompletionOptions = {} ) {} async completion( messages: ChatMessage[], options: Partial<CompletionOptions> = {} ): Promise<string> { const config = AI_PROVIDERS[this.provider]; const finalOptions = { ...this.options, ...options }; // Implementation would depend on the provider // This is a basic structure const response = await fetch(\`\${config.baseURL}/chat/completions\`, { method: 'POST', headers: { 'Authorization': \`Bearer \${config.apiKey}\`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: finalOptions.model || 'gpt-3.5-turbo', messages, temperature: finalOptions.temperature || 0.7, max_tokens: finalOptions.maxTokens || 1000, }), }); const data = await response.json(); return data.choices[0]?.message?.content || ''; } } `; await writeFile(join2(aiPath, "client.ts"), client); } async function addVercelAI(aiPath) { const vercelClient = `/** * Vercel AI SDK integration */ import { anthropic } from '@ai-sdk/anthropic'; import { google } from '@ai-sdk/google'; import { openai } from '@ai-sdk/openai'; import { generateText, streamText } from 'ai'; export const AI_MODELS = { 'gpt-4-turbo': openai('gpt-4-turbo'), 'gpt-3.5-turbo': openai('gpt-3.5-turbo'), 'claude-3-haiku': anthropic('claude-3-haiku-20240307'), 'claude-3-sonnet': anthropic('claude-3-sonnet-20240229'), 'gemini-pro': google('models/gemini-pro'), } as const; export type AIModelKey = keyof typeof AI_MODELS; export async function generateCompletion( model: AIModelKey, prompt: string, options?: { temperature?: number; maxTokens?: number; } ) { const { text } = await generateText({ model: AI_MODELS[model], prompt, temperature: options?.temperature || 0.7, maxTokens: options?.maxTokens || 1000, }); return text; } export async function streamCompletion( model: AIModelKey, prompt: string, options?: { temperature?: number; maxTokens?: number; } ) { const { textStream } = await streamText({ model: AI_MODELS[model], prompt, temperature: options?.temperature || 0.7, maxTokens: options?.maxTokens || 1000, }); return textStream; } `; await writeFile(join2(aiPath, "vercel-client.ts"), vercelClient); const hooks = `/** * React hooks for Vercel AI SDK */ 'use client'; import { useChat, useCompletion } from 'ai/react'; import { type AIModelKey } from './vercel-client.js'; export function useAIChat(model: AIModelKey = 'gpt-3.5-turbo') { return useChat({ api: '/api/chat', body: { model, }, }); } export function useAICompletion(model: AIModelKey = 'gpt-3.5-turbo') { return useCompletion({ api: '/api/completion', body: { model, }, }); } `; await writeFile(join2(aiPath, "vercel-hooks.ts"), hooks); } async function addAPIRoutes(apiPath, type) { if (type === "vercel") { const chatRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client'; import { streamText } from 'ai'; export async function POST(req: Request) { const { messages, model = 'gpt-3.5-turbo' } = await req.json(); const result = await streamText({ model: AI_MODELS[model as AIModelKey], messages, }); return result.toDataStreamResponse(); } `; await ensureDir(join2(apiPath, "chat")); await writeFile(join2(apiPath, "chat", "route.ts"), chatRoute); const completionRoute = `import { AI_MODELS, type AIModelKey } from '@/lib/ai/vercel-client'; import { generateText } from 'ai'; export async function POST(req: Request) { const { prompt, model = 'gpt-3.5-turbo' } = await req.json(); const { text } = await generateText({ model: AI_MODELS[model as AIModelKey], prompt, }); return Response.json({ text }); } `; await ensureDir(join2(apiPath, "completion")); await writeFile(join2(apiPath, "completion", "route.ts"), completionRoute); } } async function updatePackageJson(projectPath, type) { const packageJsonPath = join2(projectPath, "package.json"); if (!await pathExists(packageJsonPath)) return; const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); if (!packageJson.dependencies) packageJson.dependencies = {}; if (type === "vercel") { packageJson.dependencies.ai = "^5.0.104"; packageJson.dependencies["@ai-sdk/openai"] = "^2.0.74"; packageJson.dependencies["@ai-sdk/anthropic"] = "^2.0.50"; packageJson.dependencies["@ai-sdk/google"] = "^2.0.44"; } await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); } // src/features/add-better-auth.ts import { readFile as readFile2, writeFile as writeFile2, pathExists as pathExists2, ensureDir as ensureDir2 } from "fs-extra"; import { join as join3 } from "path"; // src/utils/cli-styling.ts import boxen from "boxen"; import chalk from "chalk"; import figures from "figures"; import gradient from "gradient-string"; var colors = { primary: chalk.hex("#2563eb"), // Blue secondary: chalk.hex("#7c3aed"), // Purple success: chalk.green, error: chalk.red, warning: chalk.yellow, info: chalk.blue, muted: chalk.gray, accent: chalk.magenta, ai: chalk.hex("#10b981") // Emerald for AI features }; var gradients = { at3: gradient("#2563eb", "#10b981"), // Blue to Emerald ai: gradient("#10b981", "#059669"), // Emerald gradient success: gradient("#22c55e", "#16a34a"), // Green gradient warning: gradient("#f59e0b", "#d97706"), // Amber gradient error: gradient("#ef4444", "#dc2626") // Red gradient }; var symbols = { success: colors.success(figures.tick), error: colors.error(figures.cross), warning: colors.warning(figures.warning), info: colors.info(figures.info), arrow: colors.muted(figures.arrowRight), bullet: colors.muted(figures.bullet), line: colors.muted(figures.line), ai: colors.ai("\u{1F916}"), stack: colors.primary("\u{1F4DA}"), edge: colors.secondary("\u26A1"), database: colors.info("\u{1F5C4}\uFE0F"), config: colors.muted("\u2699\uFE0F") }; var style = { title: (text) => gradients.at3(text), subtitle: (text) => colors.secondary(text), heading: (text) => colors.primary.bold(text), label: (text) => colors.muted(text), value: (text) => colors.primary(text), success: (text) => colors.success(text), error: (text) => colors.error(text), warning: (text) => colors.warning(text), info: (text) => colors.info(text), muted: (text) => colors.muted(text), accent: (text) => colors.accent(text), ai: (text) => colors.ai(text), code: (text) => chalk.cyan.italic(text), path: (text) => chalk.dim.underline(text), command: (text) => chalk.bgBlack.white.bold(` ${text} `) }; var header = { main: (title, subtitle) => { const titleText = gradients.at3.multiline(title); const subtitleText = subtitle ? ` ${colors.muted(subtitle)}` : ""; return `${titleText}${subtitleText}`; }, section: (title) => ` ${colors.primary.bold(title)} ${colors.muted("\u2500".repeat(title.length))}` }; // src/features/add-better-auth.ts async function addBetterAuth(projectPath) { console.log(colors.info("Adding Better Auth...")); await updatePackageJson2(projectPath); await addEnvExample(projectPath); await createAuthFiles(projectPath); console.log(colors.success("\u2713 Added Better Auth configuration")); } async function updatePackageJson2(projectPath) { const packageJsonPath = join3(projectPath, "package.json"); if (!await pathExists2(packageJsonPath)) return; const packageJson = JSON.parse(await readFile2(packageJsonPath, "utf-8")); if (!packageJson.dependencies) packageJson.dependencies = {}; packageJson.dependencies["better-auth"] = "^1.1.0"; await writeFile2(packageJsonPath, JSON.stringify(packageJson, null, 2)); } async function addEnvExample(projectPath) { const envExamplePath = join3(projectPath, ".env.example"); let envContent = ""; if (await pathExists2(envExamplePath)) { envContent = await readFile2(envExamplePath, "utf-8"); } if (!envContent.includes("BETTER_AUTH_SECRET")) { envContent += ` # Better Auth BETTER_AUTH_SECRET=your_secret_here BETTER_AUTH_URL=http://localhost:3000 `; await writeFile2(envExamplePath, envContent); } } async function createAuthFiles(projectPath) { const libPath = join3(projectPath, "src", "lib", "auth"); await ensureDir2(libPath); await writeFile2(join3(libPath, "client.ts"), `import { createAuthClient } from "better-auth/react" export const authClient = createAuthClient({ baseURL: process.env.BETTER_AUTH_URL }) `); await writeFile2(join3(libPath, "auth.ts"), `import { betterAuth } from "better-auth"; // import { db } from "@/db"; // import { drizzleAdapter } from "better-auth/adapters/drizzle"; export const auth = betterAuth({ // adapter: drizzleAdapter(db, { // provider: "pg", // }), emailAndPassword: { enabled: true }, // socialProviders: { // github: { // clientId: process.env.GITHUB_CLIENT_ID, // clientSecret: process.env.GITHUB_CLIENT_SECRET, // } // }, }) `); const apiPath = join3(projectPath, "src", "app", "api", "auth", "[...all]"); await ensureDir2(apiPath); await writeFile2(join3(apiPath, "route.ts"), `import { auth } from "@/lib/auth/auth"; import { toNextJsHandler } from "better-auth/next-js"; export const { GET, POST } = toNextJsHandler(auth); `); } // src/features/add-clerk.ts import { readFile as readFile3, writeFile as writeFile3, pathExists as pathExists3 } from "fs-extra"; import { join as join4 } from "path"; async function addClerk(projectPath) { console.log(colors.info("Adding Clerk authentication...")); await updatePackageJson3(projectPath); await addEnvExample2(projectPath); await addMiddleware(projectPath); console.log(colors.warning("NOTE: You need to wrap your root layout with <ClerkProvider> manually.")); console.log(colors.success("\u2713 Added Clerk configuration")); } async function updatePackageJson3(projectPath) { const packageJsonPath = join4(projectPath, "package.json"); if (!await pathExists3(packageJsonPath)) return; const packageJson = JSON.parse(await readFile3(packageJsonPath, "utf-8")); if (!packageJson.dependencies) packageJson.dependencies = {}; packageJson.dependencies["@clerk/nextjs"] = "^6.9.0"; await writeFile3(packageJsonPath, JSON.stringify(packageJson, null, 2)); } async function addEnvExample2(projectPath) { const envExamplePath = join4(projectPath, ".env.example"); let envContent = ""; if (await pathExists3(envExamplePath)) { envContent = await readFile3(envExamplePath, "utf-8"); } if (!envContent.includes("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")) { envContent += ` # Clerk Auth NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_... `; await writeFile3(envExamplePath, envContent); } } async function addMiddleware(projectPath) { const middlewarePath = join4(projectPath, "src", "middleware.ts"); const content = `import { clerkMiddleware } from "@clerk/nextjs/server"; export default clerkMiddleware(); export const config = { matcher: ["/((?!.*\\\\..*|_next).*)", "/", "/(api|trpc)(.*)"], }; `; await writeFile3(middlewarePath, content); } // src/features/add-drizzle.ts import { ensureDir as ensureDir3, pathExists as pathExists4, readFile as readFile4, writeFile as writeFile4 } from "fs-extra"; import { join as join5 } from "path"; async function addDrizzle(projectPath) { const srcPath = join5(projectPath, "src"); await ensureDir3(srcPath); const dbPath = join5(srcPath, "db"); await ensureDir3(dbPath); await addDrizzleConfig(projectPath); await addSchema(dbPath); await addClient(dbPath); await updatePackageJson4(projectPath); await addEnvExample3(projectPath); console.log("\u2713 Added Drizzle ORM + PostgreSQL support"); } async function addDrizzleConfig(projectPath) { const config = `import { defineConfig } from 'drizzle-kit'; import * as dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); if (!process.env.DATABASE_URL) { throw new Error('DATABASE_URL is missing'); } export default defineConfig({ schema: './src/db/schema.ts', out: './drizzle', dialect: 'postgresql', dbCredentials: { url: process.env.DATABASE_URL, }, }); `; await writeFile4(join5(projectPath, "drizzle.config.ts"), config); } async function addSchema(dbPath) { const schema = `import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name'), email: text('email').notNull(), createdAt: timestamp('created_at').defaultNow(), }); `; await writeFile4(join5(dbPath, "schema.ts"), schema); } async function addClient(dbPath) { const client = `import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; import * as schema from './schema'; const connectionString = process.env.DATABASE_URL!; // Disable prefetch as it is not supported for "Transaction" pool mode const client = postgres(connectionString, { prepare: false }); export const db = drizzle(client, { schema }); `; await writeFile4(join5(dbPath, "index.ts"), client); } async function updatePackageJson4(projectPath) { const packageJsonPath = join5(projectPath, "package.json"); if (!await pathExists4(packageJsonPath)) return; const packageJson = JSON.parse(await readFile4(packageJsonPath, "utf-8")); if (!packageJson.dependencies) packageJson.dependencies = {}; if (!packageJson.devDependencies) packageJson.devDependencies = {}; if (!packageJson.scripts) packageJson.scripts = {}; packageJson.dependencies["drizzle-orm"] = "^0.36.0"; packageJson.dependencies["postgres"] = "^3.4.4"; packageJson.dependencies["dotenv"] = "^16.4.5"; packageJson.devDependencies["drizzle-kit"] = "^0.28.0"; packageJson.devDependencies["pg"] = "^8.13.0"; packageJson.devDependencies["@types/pg"] = "^8.11.10"; packageJson.scripts["db:generate"] = "drizzle-kit generate"; packageJson.scripts["db:migrate"] = "drizzle-kit migrate"; packageJson.scripts["db:push"] = "drizzle-kit push"; packageJson.scripts["db:studio"] = "drizzle-kit studio"; await writeFile4(packageJsonPath, JSON.stringify(packageJson, null, 2)); } async function addEnvExample3(projectPath) { const envExamplePath = join5(projectPath, ".env.example"); let envContent = ""; if (await pathExists4(envExamplePath)) { envContent = await readFile4(envExamplePath, "utf-8"); } if (!envContent.includes("DATABASE_URL")) { const dbVars = ` # Database (PostgreSQL) DATABASE_URL="postgresql://postgres:password@localhost:5432/postgres" `; envContent = envContent + dbVars; await writeFile4(envExamplePath, envContent); } } // src/features/add-i18n.ts import { ensureDir as ensureDir4, readFile as readFile5, writeFile as writeFile5 } from "fs-extra"; import { join as join6 } from "path"; async function addI18n(projectPath) { const srcPath = join6(projectPath, "src"); await ensureDir4(srcPath); const messagesPath = join6(projectPath, "messages"); await ensureDir4(messagesPath); await addDefaultMessages(messagesPath); const i18nPath = join6(srcPath, "lib", "i18n"); await ensureDir4(i18nPath); await addI18nConfig(i18nPath); await addI18nRequest(i18nPath); await addI18nNavigation(i18nPath); await addI18nMiddleware(srcPath); await addLocaleProvider(i18nPath); await addLanguageSwitcher(srcPath); await addLocaleLayout(srcPath); await updatePackageJson5(projectPath); await updateNextConfig(projectPath); console.log("\u2713 Added i18n support (next-intl v4)"); } async function addDefaultMessages(messagesPath) { const enMessages = { Common: { loading: "Loading...", error: "An error occurred", retry: "Try again", cancel: "Cancel", save: "Save", delete: "Delete", edit: "Edit", back: "Back", next: "Next", previous: "Previous", search: "Search", noResults: "No results found" }, Navigation: { home: "Home", about: "About", contact: "Contact", dashboard: "Dashboard", settings: "Settings", profile: "Profile" }, Auth: { signIn: "Sign In", signOut: "Sign Out", signUp: "Sign Up", email: "Email", password: "Password", forgotPassword: "Forgot password?", rememberMe: "Remember me", noAccount: "Don't have an account?", hasAccount: "Already have an account?" }, Index: { title: "Welcome to AT3 Stack", description: "Build AI-native applications with edge deployment", getStarted: "Get Started", learnMore: "Learn More" }, Errors: { notFound: "Page not found", notFoundDescription: "The page you are looking for does not exist.", serverError: "Server error", serverErrorDescription: "Something went wrong on our end.", goHome: "Go to home" }, LanguageSwitcher: { label: "Language", en: "English", es: "Espa\xF1ol", fr: "Fran\xE7ais", de: "Deutsch", ja: "\u65E5\u672C\u8A9E", zh: "\u4E2D\u6587" } }; const esMessages = { Common: { loading: "Cargando...", error: "Ocurri\xF3 un error", retry: "Intentar de nuevo", cancel: "Cancelar", save: "Guardar", delete: "Eliminar", edit: "Editar", back: "Atr\xE1s", next: "Siguiente", previous: "Anterior", search: "Buscar", noResults: "No se encontraron resultados" }, Navigation: { home: "Inicio", about: "Acerca de", contact: "Contacto", dashboard: "Panel", settings: "Configuraci\xF3n", profile: "Perfil" }, Auth: { signIn: "Iniciar sesi\xF3n", signOut: "Cerrar sesi\xF3n", signUp: "Registrarse", email: "Correo electr\xF3nico", password: "Contrase\xF1a", forgotPassword: "\xBFOlvidaste tu contrase\xF1a?", rememberMe: "Recordarme", noAccount: "\xBFNo tienes una cuenta?", hasAccount: "\xBFYa tienes una cuenta?" }, Index: { title: "Bienvenido a AT3 Stack", description: "Construye aplicaciones nativas de IA con despliegue en el borde", getStarted: "Comenzar", learnMore: "Saber m\xE1s" }, Errors: { notFound: "P\xE1gina no encontrada", notFoundDescription: "La p\xE1gina que buscas no existe.", serverError: "Error del servidor", serverErrorDescription: "Algo sali\xF3 mal de nuestro lado.", goHome: "Ir al inicio" }, LanguageSwitcher: { label: "Idioma", en: "English", es: "Espa\xF1ol", fr: "Fran\xE7ais", de: "Deutsch", ja: "\u65E5\u672C\u8A9E", zh: "\u4E2D\u6587" } }; await writeFile5(join6(messagesPath, "en.json"), JSON.stringify(enMessages, null, 2)); await writeFile5(join6(messagesPath, "es.json"), JSON.stringify(esMessages, null, 2)); } async function addI18nConfig(i18nPath) { const config = `import { getRequestConfig } from 'next-intl/server' import { hasLocale } from 'next-intl' import { routing } from './navigation' export default getRequestConfig(async ({ requestLocale }) => { // Typically corresponds to the \`[locale]\` segment const requested = await requestLocale const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale return { locale, messages: (await import(\`../../../messages/\${locale}.json\`)).default, } }) `; await writeFile5(join6(i18nPath, "config.ts"), config); } async function addI18nRequest(i18nPath) { const request = `import { getRequestConfig } from 'next-intl/server' import { routing } from './navigation' export default getRequestConfig(async ({ requestLocale }) => { // This typically corresponds to the \`[locale]\` segment let locale = await requestLocale // Ensure that the incoming \`locale\` is valid if (!locale || !routing.locales.includes(locale as any)) { locale = routing.defaultLocale } return { locale, messages: (await import(\`../../../messages/\${locale}.json\`)).default, timeZone: 'UTC', now: new Date(), } }) `; await writeFile5(join6(i18nPath, "request.ts"), request); } async function addI18nNavigation(i18nPath) { const navigation = `import { createNavigation } from 'next-intl/navigation' import { defineRouting } from 'next-intl/routing' export const locales = ['en', 'es'] as const export const defaultLocale = 'en' as const export type Locale = (typeof locales)[number] export const routing = defineRouting({ locales, defaultLocale, localePrefix: 'as-needed', }) // Lightweight wrappers around Next.js' navigation APIs // that will consider the routing configuration export const { Link, redirect, usePathname, useRouter, getPathname } = createNavigation(routing) `; await writeFile5(join6(i18nPath, "navigation.ts"), navigation); } async function addI18nMiddleware(srcPath) { const middleware = `import createMiddleware from 'next-intl/middleware' import { routing } from './lib/i18n/navigation' export default createMiddleware(routing) export const config = { // Match all pathnames except for // - ... if they start with \`/api\`, \`/_next\` or \`/_vercel\` // - ... the ones containing a dot (e.g. \`favicon.ico\`) matcher: ['/((?!api|_next|_vercel|.*\\\\..*).*)'], } `; await writeFile5(join6(srcPath, "middleware.ts"), middleware); } async function addLocaleProvider(i18nPath) { const provider = `'use client' import { NextIntlClientProvider } from 'next-intl' import type { ReactNode } from 'react' interface LocaleProviderProps { children: ReactNode locale: string messages: Record<string, unknown> timeZone?: string now?: Date } export function LocaleProvider({ children, locale, messages, timeZone = 'UTC', now, }: LocaleProviderProps) { return ( <NextIntlClientProvider locale={locale} messages={messages} timeZone={timeZone} now={now} > {children} </NextIntlClientProvider> ) } `; await writeFile5(join6(i18nPath, "provider.tsx"), provider); const index = `export { locales, defaultLocale, routing, type Locale } from './navigation' export { Link, redirect, usePathname, useRouter, getPathname } from './navigation' export { LocaleProvider } from './provider' `; await writeFile5(join6(i18nPath, "index.ts"), index); } async function addLanguageSwitcher(srcPath) { const componentPath = join6(srcPath, "components", "layout"); await ensureDir4(componentPath); const switcher = `'use client' import { useLocale, useTranslations } from 'next-intl' import { usePathname, useRouter } from '@/lib/i18n/navigation' import { locales, type Locale } from '@/lib/i18n/navigation' interface LanguageSwitcherProps { className?: string } export function LanguageSwitcher({ className }: LanguageSwitcherProps) { const t = useTranslations('LanguageSwitcher') const locale = useLocale() const router = useRouter() const pathname = usePathname() const handleChange = (newLocale: string) => { router.replace(pathname, { locale: newLocale as Locale }) } return ( <div className={className}> <label htmlFor="language-select" className="sr-only"> {t('label')} </label> <select id="language-select" value={locale} onChange={(e) => handleChange(e.target.value)} className="rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" > {locales.map((loc) => ( <option key={loc} value={loc}> {t(loc)} </option> ))} </select> </div> ) } /** * Alternative: Language switcher with buttons */ export function LanguageSwitcherButtons({ className }: LanguageSwitcherProps) { const t = useTranslations('LanguageSwitcher') const locale = useLocale() const router = useRouter() const pathname = usePathname() return ( <div className={\`flex gap-2 \${className}\`}> {locales.map((loc) => ( <button key={loc} onClick={() => router.replace(pathname, { locale: loc })} disabled={locale === loc} className={\`px-3 py-1 text-sm rounded-md transition-colors \${ locale === loc ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground hover:bg-secondary/80' }\`} > {t(loc)} </button> ))} </div> ) } `; await writeFile5(join6(componentPath, "language-switcher.tsx"), switcher); } async function addLocaleLayout(srcPath) { const localePath = join6(srcPath, "app", "[locale]"); await ensureDir4(localePath); const layout = `import { NextIntlClientProvider, hasLocale } from 'next-intl' import { getMessages, setRequestLocale } from 'next-intl/server' import { notFound } from 'next/navigation' import type { ReactNode } from 'react' import { routing } from '@/lib/i18n/navigation' interface LocaleLayoutProps { children: ReactNode params: Promise<{ locale: string }> } export function generateStaticParams() { return routing.locales.map((locale) => ({ locale })) } export default async function LocaleLayout({ children, params, }: LocaleLayoutProps) { const { locale } = await params // Ensure that the incoming \`locale\` is valid if (!hasLocale(routing.locales, locale)) { notFound() } // Enable static rendering setRequestLocale(locale) // Providing all messages to the client // side is the easiest way to get started const messages = await getMessages() return ( <NextIntlClientProvider messages={messages}> {children} </NextIntlClientProvider> ) } `; await writeFile5(join6(localePath, "layout.tsx"), layout); const page = `import { useTranslations } from 'next-intl' import { setRequestLocale } from 'next-intl/server' import { Link } from '@/lib/i18n/navigation' interface HomePageProps { params: Promise<{ locale: string }> } export default async function HomePage({ params }: HomePageProps) { const { locale } = await params // Enable static rendering setRequestLocale(locale) return <HomeContent /> } function HomeContent() { const t = useTranslations('Index') return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-4xl font-bold mb-4">{t('title')}</h1> <p className="text-lg text-muted-foreground mb-8">{t('description')}</p> <div className="flex gap-4"> <Link href="/dashboard" className="rounded-md bg-primary px-6 py-3 text-primary-foreground hover:bg-primary/90" > {t('getStarted')} </Link> <Link href="/about" className="rounded-md border border-input bg-background px-6 py-3 hover:bg-accent hover:text-accent-foreground" > {t('learnMore')} </Link> </div> </main> ) } `; await writeFile5(join6(localePath, "page.tsx"), page); const notFound = `import { useTranslations } from 'next-intl' import { Link } from '@/lib/i18n/navigation' export default function NotFoundPage() { const t = useTranslations('Errors') return ( <main className="flex min-h-screen flex-col items-center justify-center p-24"> <h1 className="text-4xl font-bold mb-4">{t('notFound')}</h1> <p className="text-lg text-muted-foreground mb-8"> {t('notFoundDescription')} </p> <Link href="/" className="rounded-md bg-primary px-6 py-3 text-primary-foreground hover:bg-primary/90" > {t('goHome')} </Link> </main> ) } `; await writeFile5(join6(localePath, "not-found.tsx"), notFound); } async function updatePackageJson5(projectPath) { const packageJsonPath = join6(projectPath, "package.json"); const packageJson = JSON.parse(await readFile5(packageJsonPath, "utf-8")); if (!packageJson.dependencies) packageJson.dependencies = {}; packageJson.dependencies["next-intl"] = "^4.5.5"; await writeFile5(packageJsonPath, JSON.stringify(packageJson, null, 2)); } async function updateNextConfig(projectPath) { const configPath = join6(projectPath, "next.config.ts"); try { let configContent = await readFile5(configPath, "utf-8"); if (configContent.includes("withNextIntl") || configContent.includes("next-intl/plugin")) { return; } const importStatement = `import createNextIntlPlugin from 'next-intl/plugin' `; const pluginInit = `const withNextIntl = createNextIntlPlugin('./src/lib/i18n/request.ts') `; if (!configContent.includes("next-intl/plugin")) { configContent = importStatement + configContent; } const exportDefaultRegex = /export\s+default\s+(\w+);?\s*$/m; const match = configContent.match(exportDefaultRegex); if (match) { const configName = match[1]; configContent = configContent.replace( exportDefaultRegex, `${pluginInit}export default withNextIntl(${configName}) ` ); } else { const inlineExportRegex = /export\s+default\s+({[\s\S]*?})\s*;?\s*$/m; const inlineMatch = configContent.match(inlineExportRegex); if (inlineMatch) { configContent = configContent.replace( inlineExportRegex, `const nextConfig = ${inlineMatch[1]} ${pluginInit}export default withNextIntl(nextConfig) ` ); } } await writeFile5(configPath, configContent); } catch (error) { console.warn( "Could not update next.config.ts automatically. Please wrap your config with withNextIntl manually." ); } } // src/features/add-pwa.ts import { join as join7 } from "path"; import { ensureDir as ensureDir5, readFile as readFile6, writeFile as writeFile6 } from "fs-extra"; async function addPWA(projectPath) { const srcPath = join7(projectPath, "src"); const publicPath = join7(projectPath, "public"); await ensureDir5(publicPath); await ensureDir5(join7(publicPath, "icons")); await addManifest(publicPath); await addServiceWorker(publicPath); await addOfflinePage(srcPath); await addPWAUtils(srcPath); await addInstallPrompt(srcPath); await addPWAProvider(srcPath); await updatePackageJson6(projectPath); await updateNextConfig2(projectPath); await updateLayout(srcPath); console.log("\u2713 PWA support added"); } async function addManifest(publicPath) { const manifest = { name: "AT3 App", short_name: "AT3", description: "AT3 Stack Application - AI-native, edge-first", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#6366f1", orientation: "portrait-primary", scope: "/", lang: "en", categories: ["productivity", "utilities"], icons: [ { src: "/icons/icon-72x72.png", sizes: "72x72", type: "image/png", purpose: "any" }, { src: "/icons/icon-96x96.png", sizes: "96x96", type: "image/png", purpose: "any" }, { src: "/icons/icon-128x128.png", sizes: "128x128", type: "image/png", purpose: "any" }, { src: "/icons/icon-144x144.png", sizes: "144x144", type: "image/png", pur