kofi-stack-template-generator 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +20 -0
  2. package/dist/index.d.ts +94 -0
  3. package/dist/index.js +744 -0
  4. package/package.json +29 -0
  5. package/scripts/generate-templates.js +104 -0
  6. package/src/core/index.ts +7 -0
  7. package/src/core/template-processor.ts +127 -0
  8. package/src/core/virtual-fs.ts +189 -0
  9. package/src/generator.ts +429 -0
  10. package/src/index.ts +19 -0
  11. package/src/templates.generated.ts +39 -0
  12. package/templates/base/_gitignore.hbs +45 -0
  13. package/templates/base/biome.json.hbs +34 -0
  14. package/templates/convex/_env.local.hbs +52 -0
  15. package/templates/convex/convex/auth.ts.hbs +7 -0
  16. package/templates/convex/convex/http.ts.hbs +8 -0
  17. package/templates/convex/convex/schema.ts.hbs +15 -0
  18. package/templates/convex/convex/users.ts.hbs +13 -0
  19. package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +17 -0
  20. package/templates/monorepo/package.json.hbs +29 -0
  21. package/templates/monorepo/pnpm-workspace.yaml.hbs +3 -0
  22. package/templates/monorepo/turbo.json.hbs +42 -0
  23. package/templates/packages/config-biome/biome.json.hbs +4 -0
  24. package/templates/packages/config-biome/package.json.hbs +6 -0
  25. package/templates/packages/config-typescript/base.json.hbs +17 -0
  26. package/templates/packages/config-typescript/nextjs.json.hbs +7 -0
  27. package/templates/packages/config-typescript/package.json.hbs +10 -0
  28. package/templates/packages/ui/components.json.hbs +20 -0
  29. package/templates/packages/ui/package.json.hbs +34 -0
  30. package/templates/packages/ui/src/index.ts.hbs +3 -0
  31. package/templates/packages/ui/src/lib/utils.ts.hbs +6 -0
  32. package/templates/packages/ui/tsconfig.json.hbs +22 -0
  33. package/templates/web/components.json.hbs +20 -0
  34. package/templates/web/next.config.ts.hbs +9 -0
  35. package/templates/web/package.json.hbs +62 -0
  36. package/templates/web/postcss.config.mjs.hbs +5 -0
  37. package/templates/web/src/app/globals.css.hbs +122 -0
  38. package/templates/web/src/app/layout.tsx.hbs +55 -0
  39. package/templates/web/src/app/page.tsx.hbs +74 -0
  40. package/templates/web/src/components/providers/convex-provider.tsx.hbs +18 -0
  41. package/templates/web/src/lib/auth.ts.hbs +23 -0
  42. package/templates/web/src/lib/utils.ts.hbs +6 -0
  43. package/templates/web/tsconfig.json.hbs +23 -0
  44. package/tsconfig.json +15 -0
package/dist/index.js ADDED
@@ -0,0 +1,744 @@
1
+ // src/core/virtual-fs.ts
2
+ import { createFsFromVolume, Volume } from "memfs";
3
+ import path from "path";
4
+ var VirtualFileSystem = class {
5
+ volume;
6
+ fs;
7
+ binarySourcePaths = /* @__PURE__ */ new Map();
8
+ constructor() {
9
+ this.volume = new Volume();
10
+ this.fs = createFsFromVolume(this.volume);
11
+ }
12
+ /**
13
+ * Write a file to the virtual filesystem
14
+ */
15
+ writeFile(filePath, content) {
16
+ const dir = path.dirname(filePath);
17
+ this.mkdir(dir);
18
+ this.fs.writeFileSync(filePath, content);
19
+ }
20
+ /**
21
+ * Read a file from the virtual filesystem
22
+ */
23
+ readFile(filePath) {
24
+ return this.fs.readFileSync(filePath);
25
+ }
26
+ /**
27
+ * Check if a path exists
28
+ */
29
+ exists(filePath) {
30
+ try {
31
+ this.fs.statSync(filePath);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ /**
38
+ * Check if path is a file
39
+ */
40
+ fileExists(filePath) {
41
+ try {
42
+ return this.fs.statSync(filePath).isFile();
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Check if path is a directory
49
+ */
50
+ directoryExists(filePath) {
51
+ try {
52
+ return this.fs.statSync(filePath).isDirectory();
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+ /**
58
+ * Create directory recursively
59
+ */
60
+ mkdir(dirPath) {
61
+ if (!this.exists(dirPath)) {
62
+ this.fs.mkdirSync(dirPath, { recursive: true });
63
+ }
64
+ }
65
+ /**
66
+ * Delete a file
67
+ */
68
+ deleteFile(filePath) {
69
+ if (this.fileExists(filePath)) {
70
+ this.fs.unlinkSync(filePath);
71
+ }
72
+ }
73
+ /**
74
+ * List directory contents
75
+ */
76
+ listDir(dirPath) {
77
+ if (!this.directoryExists(dirPath)) {
78
+ return [];
79
+ }
80
+ return this.fs.readdirSync(dirPath);
81
+ }
82
+ /**
83
+ * Track source path for binary files (for later copying)
84
+ */
85
+ setBinarySourcePath(virtualPath, sourcePath) {
86
+ this.binarySourcePaths.set(virtualPath, sourcePath);
87
+ }
88
+ /**
89
+ * Get source path for binary file
90
+ */
91
+ getBinarySourcePath(virtualPath) {
92
+ return this.binarySourcePaths.get(virtualPath);
93
+ }
94
+ /**
95
+ * Convert the virtual filesystem to a tree structure
96
+ */
97
+ toTree(config) {
98
+ const root = this.buildTree("/");
99
+ const stats = this.countNodes(root);
100
+ return {
101
+ root,
102
+ fileCount: stats.files,
103
+ directoryCount: stats.directories,
104
+ config
105
+ };
106
+ }
107
+ buildTree(dirPath) {
108
+ const name = dirPath === "/" ? "/" : path.basename(dirPath);
109
+ const children = [];
110
+ const entries = this.listDir(dirPath);
111
+ for (const entry of entries) {
112
+ const fullPath = path.join(dirPath, entry);
113
+ const stat = this.fs.statSync(fullPath);
114
+ if (stat.isDirectory()) {
115
+ children.push(this.buildTree(fullPath));
116
+ } else {
117
+ const content = this.fs.readFileSync(fullPath);
118
+ const file = {
119
+ type: "file",
120
+ path: fullPath,
121
+ name: entry,
122
+ content,
123
+ extension: path.extname(entry),
124
+ sourcePath: this.binarySourcePaths.get(fullPath)
125
+ };
126
+ children.push(file);
127
+ }
128
+ }
129
+ return {
130
+ type: "directory",
131
+ path: dirPath,
132
+ name,
133
+ children
134
+ };
135
+ }
136
+ countNodes(node) {
137
+ if (node.type === "file") {
138
+ return { files: 1, directories: 0 };
139
+ }
140
+ let files = 0;
141
+ let directories = 1;
142
+ for (const child of node.children) {
143
+ const counts = this.countNodes(child);
144
+ files += counts.files;
145
+ directories += counts.directories;
146
+ }
147
+ return { files, directories };
148
+ }
149
+ /**
150
+ * Get the raw memfs instance for advanced operations
151
+ */
152
+ getRawFs() {
153
+ return this.fs;
154
+ }
155
+ };
156
+
157
+ // src/core/template-processor.ts
158
+ import Handlebars from "handlebars";
159
+ import path2 from "path";
160
+ Handlebars.registerHelper("eq", (a, b) => a === b);
161
+ Handlebars.registerHelper("ne", (a, b) => a !== b);
162
+ Handlebars.registerHelper("and", (...args) => {
163
+ const values = args.slice(0, -1);
164
+ return values.every(Boolean);
165
+ });
166
+ Handlebars.registerHelper("or", (...args) => {
167
+ const values = args.slice(0, -1);
168
+ return values.some(Boolean);
169
+ });
170
+ Handlebars.registerHelper("includes", (array, value) => {
171
+ if (!Array.isArray(array)) return false;
172
+ return array.includes(value);
173
+ });
174
+ Handlebars.registerHelper("not", (value) => !value);
175
+ Handlebars.registerHelper("json", (value) => JSON.stringify(value, null, 2));
176
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
177
+ ".png",
178
+ ".jpg",
179
+ ".jpeg",
180
+ ".gif",
181
+ ".ico",
182
+ ".webp",
183
+ ".svg",
184
+ ".woff",
185
+ ".woff2",
186
+ ".ttf",
187
+ ".eot",
188
+ ".otf",
189
+ ".mp3",
190
+ ".mp4",
191
+ ".webm",
192
+ ".pdf",
193
+ ".zip",
194
+ ".tar",
195
+ ".gz"
196
+ ]);
197
+ function isBinaryFile(filename) {
198
+ const ext = path2.extname(filename).toLowerCase();
199
+ return BINARY_EXTENSIONS.has(ext);
200
+ }
201
+ function processTemplateString(template, config) {
202
+ try {
203
+ const compiled = Handlebars.compile(template, { noEscape: true });
204
+ return compiled(config);
205
+ } catch (error) {
206
+ console.error("Template processing error:", error);
207
+ return template;
208
+ }
209
+ }
210
+ function transformFilename(filename, config) {
211
+ let result = filename;
212
+ if (result.endsWith(".hbs")) {
213
+ result = result.slice(0, -4);
214
+ }
215
+ if (result.startsWith("_")) {
216
+ result = "." + result.slice(1);
217
+ }
218
+ if (result.includes("{{")) {
219
+ result = processTemplateString(result, config);
220
+ }
221
+ return result;
222
+ }
223
+ function shouldIncludeFile(templatePath, config) {
224
+ if (templatePath.includes("/if-monorepo/") && config.structure !== "monorepo") {
225
+ return false;
226
+ }
227
+ if (templatePath.includes("/if-standalone/") && config.structure !== "standalone") {
228
+ return false;
229
+ }
230
+ if (templatePath.includes("/if-payload/") && config.marketingSite !== "payload") {
231
+ return false;
232
+ }
233
+ if (templatePath.includes("/if-posthog/") && config.integrations.analytics !== "posthog") {
234
+ return false;
235
+ }
236
+ if (templatePath.includes("/if-uploadthing/") && config.integrations.uploads !== "uploadthing") {
237
+ return false;
238
+ }
239
+ return true;
240
+ }
241
+
242
+ // src/templates.generated.ts
243
+ var EMBEDDED_TEMPLATES = {
244
+ "base/_gitignore.hbs": "# Dependencies\nnode_modules\n.pnpm-store\n\n# Build outputs\n.next\ndist\n.turbo\nout\n\n# Testing\ncoverage\nplaywright-report\ntest-results\n\n# Environment\n.env\n.env.local\n.env.*.local\n\n# IDE\n.idea\n.vscode\n*.swp\n*.swo\n.DS_Store\n\n# Convex\n.convex\n\n# Vercel\n.vercel\n\n# Debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# TypeScript\n*.tsbuildinfo\n\n# Misc\n*.pem\n.cache\n",
245
+ "base/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "organizeImports": {\n "enabled": true\n },\n "linter": {\n "enabled": true,\n "rules": {\n "recommended": true\n }\n },\n "formatter": {\n "enabled": true,\n "indentStyle": "space",\n "indentWidth": 2\n },\n "javascript": {\n "formatter": {\n "quoteStyle": "single",\n "semicolons": "asNeeded"\n }\n },\n "files": {\n "ignore": [\n "node_modules",\n ".next",\n "dist",\n ".turbo",\n "coverage",\n ".vercel",\n "_generated"\n ]\n }\n}\n',
246
+ "convex/_env.local.hbs": "# Convex\nCONVEX_DEPLOYMENT=\nNEXT_PUBLIC_CONVEX_URL=\n\n# Auth - GitHub OAuth\nAUTH_GITHUB_ID=\nAUTH_GITHUB_SECRET=\n\n# Auth - Google OAuth\nAUTH_GOOGLE_ID=\nAUTH_GOOGLE_SECRET=\n\n# Better Auth Secret (generate with: openssl rand -base64 32)\nBETTER_AUTH_SECRET=\n{{#if (eq integrations.analytics 'posthog')}}\n\n# PostHog\nNEXT_PUBLIC_POSTHOG_KEY=\nNEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com\n{{/if}}\n{{#if (eq integrations.uploads 'uploadthing')}}\n\n# UploadThing\nUPLOADTHING_TOKEN=\n{{/if}}\n{{#if (eq integrations.uploads 's3')}}\n\n# AWS S3\nAWS_ACCESS_KEY_ID=\nAWS_SECRET_ACCESS_KEY=\nAWS_REGION=\nAWS_S3_BUCKET=\n{{/if}}\n{{#if (eq integrations.uploads 'vercel-blob')}}\n\n# Vercel Blob\nBLOB_READ_WRITE_TOKEN=\n{{/if}}\n{{#if (includes addons 'rate-limiting')}}\n\n# Arcjet\nARCJET_KEY=\n{{/if}}\n{{#if (includes addons 'monitoring')}}\n\n# Sentry\nSENTRY_DSN=\nSENTRY_AUTH_TOKEN=\n{{/if}}\n\n# Email (Resend)\nRESEND_API_KEY=\n",
247
+ "convex/convex/auth.ts.hbs": "import GitHub from '@auth/core/providers/github'\nimport Google from '@auth/core/providers/google'\nimport { convexAuth } from '@convex-dev/auth/server'\n\nexport const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({\n providers: [GitHub, Google],\n})\n",
248
+ "convex/convex/http.ts.hbs": "import { httpRouter } from 'convex/server'\nimport { auth } from './auth'\n\nconst http = httpRouter()\n\nauth.addHttpRoutes(http)\n\nexport default http\n",
249
+ "convex/convex/schema.ts.hbs": "import { defineSchema, defineTable } from 'convex/server'\nimport { authTables } from '@convex-dev/auth/server'\nimport { v } from 'convex/values'\n\nexport default defineSchema({\n ...authTables,\n // Add your custom tables here\n // Example:\n // posts: defineTable({\n // title: v.string(),\n // content: v.string(),\n // authorId: v.id('users'),\n // createdAt: v.number(),\n // }).index('by_author', ['authorId']),\n})\n",
250
+ "convex/convex/users.ts.hbs": "import { query } from './_generated/server'\nimport { auth } from './auth'\n\nexport const current = query({\n args: {},\n handler: async (ctx) => {\n const userId = await auth.getUserId(ctx)\n if (!userId) return null\n\n const user = await ctx.db.get(userId)\n return user\n },\n})\n",
251
+ "integrations/posthog/src/components/providers/posthog-provider.tsx.hbs": "'use client'\n\nimport posthog from 'posthog-js'\nimport { PostHogProvider as PHProvider } from 'posthog-js/react'\nimport { useEffect } from 'react'\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n useEffect(() => {\n posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',\n person_profiles: 'identified_only',\n capture_pageview: false, // We capture pageviews manually\n })\n }, [])\n\n return <PHProvider client={posthog}>{children}</PHProvider>\n}\n",
252
+ "monorepo/package.json.hbs": '{\n "name": "{{projectName}}",\n "version": "0.1.0",\n "private": true,\n "scripts": {\n "dev": "node scripts/dev.mjs",\n "dev:turbo": "turbo dev",\n "build": "turbo build",\n "lint": "turbo lint",\n "lint:fix": "turbo lint:fix",\n "format": "turbo format",\n "typecheck": "turbo typecheck",\n "test": "turbo test",\n "test:e2e": "turbo test:e2e",\n "clean": "turbo clean && rm -rf node_modules",\n "prepare": "husky",\n "setup:convex": "node apps/web/scripts/setup-convex.mjs"\n },\n "devDependencies": {\n "turbo": "^2.0.0",\n "husky": "^9.0.0",\n "lint-staged": "^15.0.0"\n },\n "packageManager": "pnpm@9.0.0",\n "lint-staged": {\n "*.{js,ts,jsx,tsx}": ["biome check --apply"],\n "*.{json,md}": ["biome format --write"]\n }\n}\n',
253
+ "monorepo/pnpm-workspace.yaml.hbs": 'packages:\n - "apps/*"\n - "packages/*"\n',
254
+ "monorepo/turbo.json.hbs": '{\n "$schema": "https://turbo.build/schema.json",\n "ui": "tui",\n "tasks": {\n "build": {\n "dependsOn": ["^build"],\n "inputs": ["$TURBO_DEFAULT$", ".env*"],\n "outputs": [".next/**", "!.next/cache/**", "dist/**"]\n },\n "lint": {\n "dependsOn": ["^lint"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "lint:fix": {\n "dependsOn": ["^lint:fix"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "format": {\n "dependsOn": ["^format"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "typecheck": {\n "dependsOn": ["^typecheck"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "dev": {\n "cache": false,\n "persistent": true\n },\n "test": {\n "dependsOn": ["^build"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "test:e2e": {\n "dependsOn": ["build"],\n "inputs": ["$TURBO_DEFAULT$"]\n },\n "clean": {\n "cache": false\n }\n }\n}\n',
255
+ "packages/config-biome/biome.json.hbs": '{\n "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",\n "extends": ["../../biome.json"]\n}\n',
256
+ "packages/config-biome/package.json.hbs": '{\n "name": "@repo/config-biome",\n "version": "0.1.0",\n "private": true,\n "files": ["biome.json"]\n}\n',
257
+ "packages/config-typescript/base.json.hbs": '{\n "compilerOptions": {\n "target": "ES2020",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "incremental": true\n },\n "exclude": ["node_modules"]\n}\n',
258
+ "packages/config-typescript/nextjs.json.hbs": '{\n "extends": "./base.json",\n "compilerOptions": {\n "jsx": "react-jsx",\n "plugins": [{ "name": "next" }]\n }\n}\n',
259
+ "packages/config-typescript/package.json.hbs": '{\n "name": "@repo/config-typescript",\n "version": "0.1.0",\n "private": true,\n "exports": {\n "./base.json": "./base.json",\n "./nextjs.json": "./nextjs.json"\n },\n "files": ["base.json", "nextjs.json"]\n}\n',
260
+ "packages/ui/components.json.hbs": '{\n "$schema": "https://ui.shadcn.com/schema.json",\n "style": "{{shadcn.componentLibrary}}-{{shadcn.styleVariant}}",\n "rsc": false,\n "tsx": true,\n "tailwind": {\n "config": "",\n "css": "",\n "baseColor": "{{shadcn.baseColor}}",\n "cssVariables": true\n },\n "iconLibrary": "{{shadcn.iconLibrary}}",\n "aliases": {\n "components": "@/components",\n "utils": "@/lib/utils",\n "ui": "@/components/ui",\n "lib": "@/lib",\n "hooks": "@/hooks"\n }\n}\n',
261
+ "packages/ui/package.json.hbs": '{\n "name": "@repo/ui",\n "version": "0.1.0",\n "private": true,\n "main": "./src/index.ts",\n "types": "./src/index.ts",\n "exports": {\n ".": "./src/index.ts",\n "./components/*": "./src/components/*.tsx",\n "./lib/*": "./src/lib/*.ts"\n },\n "scripts": {\n "lint": "biome check .",\n "lint:fix": "biome check --write .",\n "typecheck": "tsc --noEmit"\n },\n "dependencies": {\n "@hugeicons/react": "^0.3.0",\n "class-variance-authority": "^0.7.0",\n "clsx": "^2.1.0",\n "tailwind-merge": "^2.5.0"\n },\n "devDependencies": {\n "@repo/config-typescript": "workspace:*",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "react": "^19.0.0",\n "tailwindcss": "^4.0.0",\n "typescript": "^5.0.0"\n },\n "peerDependencies": {\n "react": "^19.0.0"\n }\n}\n',
262
+ "packages/ui/src/index.ts.hbs": "export { cn } from './lib/utils'\n// Export components as they are added\n// export * from './components/ui/button'\n",
263
+ "packages/ui/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
264
+ "packages/ui/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2020",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["src/**/*"],\n "exclude": ["node_modules"]\n}\n',
265
+ "web/components.json.hbs": '{\n "$schema": "https://ui.shadcn.com/schema.json",\n "style": "{{shadcn.componentLibrary}}-{{shadcn.styleVariant}}",\n "rsc": true,\n "tsx": true,\n "tailwind": {\n "config": "",\n "css": "src/app/globals.css",\n "baseColor": "{{shadcn.baseColor}}",\n "cssVariables": true\n },\n "iconLibrary": "{{shadcn.iconLibrary}}",\n "aliases": {\n "components": "@/components",\n "utils": "@/lib/utils",\n "ui": "@/components/ui",\n "lib": "@/lib",\n "hooks": "@/hooks"\n }\n}\n',
266
+ "web/next.config.ts.hbs": "import type { NextConfig } from 'next'\n\nconst nextConfig: NextConfig = {\n{{#if (eq structure 'monorepo')}}\n transpilePackages: ['@repo/ui', '@repo/backend'],\n{{/if}}\n}\n\nexport default nextConfig\n",
267
+ "web/package.json.hbs": `{
268
+ "name": "{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}",
269
+ "version": "0.1.0",
270
+ "private": true,
271
+ "type": "module",
272
+ "scripts": {
273
+ "dev": "{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}",
274
+ "dev:next": "next dev --turbopack",
275
+ "build": "next build",
276
+ "start": "next start",
277
+ "lint": "biome check .",
278
+ "lint:fix": "biome check --write .",
279
+ "typecheck": "tsc --noEmit",
280
+ "test": "vitest run",
281
+ "test:watch": "vitest",
282
+ "test:e2e": "playwright test",
283
+ "setup:convex": "node scripts/setup-convex.mjs"
284
+ },
285
+ "dependencies": {
286
+ "next": "^16.0.0",
287
+ "react": "^19.0.0",
288
+ "react-dom": "^19.0.0",
289
+ "convex": "^1.25.0",
290
+ "@convex-dev/auth": "^0.0.90",
291
+ "@auth/core": "^0.37.0",
292
+ "@hugeicons/react": "^0.3.0",
293
+ "class-variance-authority": "^0.7.0",
294
+ "clsx": "^2.1.0",
295
+ "tailwind-merge": "^2.5.0",
296
+ "tw-animate-css": "^1.3.0",
297
+ "resend": "^4.0.0",
298
+ "react-email": "^3.0.0",
299
+ "@react-email/components": "^0.0.36"{{#if (eq integrations.analytics 'posthog')}},
300
+ "posthog-js": "^1.200.0",
301
+ "posthog-node": "^5.0.0"{{/if}}{{#if (eq integrations.analytics 'vercel')}},
302
+ "@vercel/analytics": "^1.4.0",
303
+ "@vercel/speed-insights": "^1.1.0"{{/if}}{{#if (eq integrations.uploads 'uploadthing')}},
304
+ "uploadthing": "^7.0.0",
305
+ "@uploadthing/react": "^7.0.0"{{/if}}{{#if (eq integrations.uploads 's3')}},
306
+ "@aws-sdk/client-s3": "^3.700.0",
307
+ "@aws-sdk/s3-request-presigner": "^3.700.0"{{/if}}{{#if (eq integrations.uploads 'vercel-blob')}},
308
+ "@vercel/blob": "^2.0.0"{{/if}}{{#if (includes addons 'rate-limiting')}},
309
+ "@arcjet/next": "^1.0.0-beta.16"{{/if}}{{#if (includes addons 'monitoring')}},
310
+ "@sentry/nextjs": "^8.0.0"{{/if}}
311
+ },
312
+ "devDependencies": {
313
+ {{#if (eq structure 'monorepo')}} "@repo/config-typescript": "workspace:*",
314
+ {{/if}} "@types/node": "^20.0.0",
315
+ "@types/react": "^19.0.0",
316
+ "@types/react-dom": "^19.0.0",
317
+ "tailwindcss": "^4.0.0",
318
+ "@tailwindcss/postcss": "^4.0.0",
319
+ "postcss": "^8.4.0",
320
+ "typescript": "^5.0.0",
321
+ "vitest": "^3.0.0",
322
+ "@vitejs/plugin-react": "^4.3.0",
323
+ "@testing-library/react": "^16.0.0",
324
+ "jsdom": "^26.0.0",
325
+ "playwright": "^1.50.0",
326
+ "@playwright/test": "^1.50.0"
327
+ }
328
+ }
329
+ `,
330
+ "web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
331
+ "web/src/app/globals.css.hbs": '@import "tailwindcss";\n@import "tw-animate-css";\n\n@custom-variant dark (&:is(.dark *));\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n --color-sidebar-ring: var(--sidebar-ring);\n --color-sidebar-border: var(--sidebar-border);\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n --color-sidebar-accent: var(--sidebar-accent);\n --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n --color-sidebar-primary: var(--sidebar-primary);\n --color-sidebar-foreground: var(--sidebar-foreground);\n --color-sidebar: var(--sidebar);\n --color-chart-5: var(--chart-5);\n --color-chart-4: var(--chart-4);\n --color-chart-3: var(--chart-3);\n --color-chart-2: var(--chart-2);\n --color-chart-1: var(--chart-1);\n --color-ring: var(--ring);\n --color-input: var(--input);\n --color-border: var(--border);\n --color-destructive: var(--destructive);\n --color-accent-foreground: var(--accent-foreground);\n --color-accent: var(--accent);\n --color-muted-foreground: var(--muted-foreground);\n --color-muted: var(--muted);\n --color-secondary-foreground: var(--secondary-foreground);\n --color-secondary: var(--secondary);\n --color-primary-foreground: var(--primary-foreground);\n --color-primary: var(--primary);\n --color-popover-foreground: var(--popover-foreground);\n --color-popover: var(--popover);\n --color-card-foreground: var(--card-foreground);\n --color-card: var(--card);\n --radius-sm: calc(var(--radius) - 4px);\n --radius-md: calc(var(--radius) - 2px);\n --radius-lg: var(--radius);\n --radius-xl: calc(var(--radius) + 4px);\n}\n\n:root {\n --radius: 0.625rem;\n --background: oklch(1 0 0);\n --foreground: oklch(0.145 0 0);\n --card: oklch(1 0 0);\n --card-foreground: oklch(0.145 0 0);\n --popover: oklch(1 0 0);\n --popover-foreground: oklch(0.145 0 0);\n --primary: oklch(0.205 0 0);\n --primary-foreground: oklch(0.985 0 0);\n --secondary: oklch(0.97 0 0);\n --secondary-foreground: oklch(0.205 0 0);\n --muted: oklch(0.97 0 0);\n --muted-foreground: oklch(0.556 0 0);\n --accent: oklch(0.97 0 0);\n --accent-foreground: oklch(0.205 0 0);\n --destructive: oklch(0.577 0.245 27.325);\n --border: oklch(0.922 0 0);\n --input: oklch(0.922 0 0);\n --ring: oklch(0.708 0 0);\n --chart-1: oklch(0.646 0.222 41.116);\n --chart-2: oklch(0.6 0.118 184.704);\n --chart-3: oklch(0.398 0.07 227.392);\n --chart-4: oklch(0.828 0.189 84.429);\n --chart-5: oklch(0.769 0.188 70.08);\n --sidebar: oklch(0.985 0 0);\n --sidebar-foreground: oklch(0.145 0 0);\n --sidebar-primary: oklch(0.205 0 0);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.97 0 0);\n --sidebar-accent-foreground: oklch(0.205 0 0);\n --sidebar-border: oklch(0.922 0 0);\n --sidebar-ring: oklch(0.708 0 0);\n}\n\n.dark {\n --background: oklch(0.145 0 0);\n --foreground: oklch(0.985 0 0);\n --card: oklch(0.205 0 0);\n --card-foreground: oklch(0.985 0 0);\n --popover: oklch(0.205 0 0);\n --popover-foreground: oklch(0.985 0 0);\n --primary: oklch(0.922 0 0);\n --primary-foreground: oklch(0.205 0 0);\n --secondary: oklch(0.269 0 0);\n --secondary-foreground: oklch(0.985 0 0);\n --muted: oklch(0.269 0 0);\n --muted-foreground: oklch(0.708 0 0);\n --accent: oklch(0.269 0 0);\n --accent-foreground: oklch(0.985 0 0);\n --destructive: oklch(0.704 0.191 22.216);\n --border: oklch(1 0 0 / 10%);\n --input: oklch(1 0 0 / 15%);\n --ring: oklch(0.556 0 0);\n --chart-1: oklch(0.488 0.243 264.376);\n --chart-2: oklch(0.696 0.17 162.48);\n --chart-3: oklch(0.769 0.188 70.08);\n --chart-4: oklch(0.627 0.265 303.9);\n --chart-5: oklch(0.645 0.246 16.439);\n --sidebar: oklch(0.205 0 0);\n --sidebar-foreground: oklch(0.985 0 0);\n --sidebar-primary: oklch(0.488 0.243 264.376);\n --sidebar-primary-foreground: oklch(0.985 0 0);\n --sidebar-accent: oklch(0.269 0 0);\n --sidebar-accent-foreground: oklch(0.985 0 0);\n --sidebar-border: oklch(1 0 0 / 10%);\n --sidebar-ring: oklch(0.556 0 0);\n}\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n body {\n @apply bg-background text-foreground;\n }\n}\n',
332
+ "web/src/app/layout.tsx.hbs": "import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server'\nimport { ConvexClientProvider } from '@/components/providers/convex-provider'\n{{#if (eq integrations.analytics 'posthog')}}\nimport { PostHogProvider } from '@/components/providers/posthog-provider'\n{{/if}}\n{{#if (eq integrations.analytics 'vercel')}}\nimport { Analytics } from '@vercel/analytics/react'\nimport { SpeedInsights } from '@vercel/speed-insights/next'\n{{/if}}\nimport './globals.css'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '{{projectName}}',\n description: 'Built with create-kofi-stack',\n}\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsServerProvider>\n <html lang=\"en\" suppressHydrationWarning>\n <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>\n <ConvexClientProvider>\n {{#if (eq integrations.analytics 'posthog')}}\n <PostHogProvider>\n {children}\n </PostHogProvider>\n {{else}}\n {children}\n {{/if}}\n </ConvexClientProvider>\n {{#if (eq integrations.analytics 'vercel')}}\n <Analytics />\n <SpeedInsights />\n {{/if}}\n </body>\n </html>\n </ConvexAuthNextjsServerProvider>\n )\n}\n",
333
+ "web/src/app/page.tsx.hbs": `'use client'
334
+
335
+ import { useAuth } from '@/lib/auth'
336
+
337
+ export default function HomePage() {
338
+ const { isAuthenticated, isLoading, signIn, signOut, user } = useAuth()
339
+
340
+ if (isLoading) {
341
+ return (
342
+ <main className="min-h-screen flex items-center justify-center">
343
+ <div className="animate-pulse">Loading...</div>
344
+ </main>
345
+ )
346
+ }
347
+
348
+ return (
349
+ <main className="min-h-screen flex flex-col items-center justify-center p-8">
350
+ <div className="max-w-2xl text-center space-y-8">
351
+ <h1 className="text-4xl font-bold">{{projectName}}</h1>
352
+ <p className="text-xl text-muted-foreground">
353
+ Built with Next.js, Convex, Better-Auth, and shadcn/ui
354
+ </p>
355
+
356
+ {isAuthenticated ? (
357
+ <div className="space-y-4">
358
+ <p className="text-lg">
359
+ Welcome, <span className="font-semibold">{user?.name || user?.email}</span>!
360
+ </p>
361
+ <button
362
+ onClick={() => signOut()}
363
+ className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
364
+ >
365
+ Sign Out
366
+ </button>
367
+ </div>
368
+ ) : (
369
+ <div className="space-y-4">
370
+ <p className="text-muted-foreground">
371
+ Sign in to get started
372
+ </p>
373
+ <div className="flex gap-4 justify-center">
374
+ <button
375
+ onClick={() => signIn('github')}
376
+ className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition"
377
+ >
378
+ Sign in with GitHub
379
+ </button>
380
+ <button
381
+ onClick={() => signIn('google')}
382
+ className="px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition"
383
+ >
384
+ Sign in with Google
385
+ </button>
386
+ </div>
387
+ </div>
388
+ )}
389
+
390
+ <div className="pt-8 border-t border-border">
391
+ <p className="text-sm text-muted-foreground">
392
+ Created with{' '}
393
+ <a
394
+ href="https://github.com/theodenanyoh11/create-kofi-stack"
395
+ className="text-primary hover:underline"
396
+ target="_blank"
397
+ rel="noopener noreferrer"
398
+ >
399
+ create-kofi-stack
400
+ </a>
401
+ </p>
402
+ </div>
403
+ </div>
404
+ </main>
405
+ )
406
+ }
407
+ `,
408
+ "web/src/components/providers/convex-provider.tsx.hbs": "'use client'\n\nimport { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs'\nimport { ConvexReactClient } from 'convex/react'\n\nconst convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!)\n\nexport function ConvexClientProvider({\n children,\n}: {\n children: React.ReactNode\n}) {\n return (\n <ConvexAuthNextjsProvider client={convex}>\n {children}\n </ConvexAuthNextjsProvider>\n )\n}\n",
409
+ "web/src/lib/auth.ts.hbs": "'use client'\n\nimport { useConvexAuth, useMutation, useQuery } from 'convex/react'\nimport { useAuthActions } from '@convex-dev/auth/react'\nimport { api } from '{{#if (eq structure 'monorepo')}}@repo/backend{{else}}../../convex/_generated/api{{/if}}'\n\nexport function useAuth() {\n const { isAuthenticated, isLoading } = useConvexAuth()\n const { signIn, signOut } = useAuthActions()\n const user = useQuery(api.users.current)\n\n return {\n isAuthenticated,\n isLoading,\n user,\n signIn: (provider: 'github' | 'google') => {\n void signIn(provider)\n },\n signOut: () => {\n void signOut()\n },\n }\n}\n",
410
+ "web/src/lib/utils.ts.hbs": "import { clsx, type ClassValue } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}\n",
411
+ "web/tsconfig.json.hbs": '{\n "compilerOptions": {\n "target": "ES2017",\n "lib": ["dom", "dom.iterable", "esnext"],\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "plugins": [{ "name": "next" }],\n "paths": {\n "@/*": ["./src/*"]\n }\n },\n "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],\n "exclude": ["node_modules"]\n}\n'
412
+ };
413
+
414
+ // src/generator.ts
415
+ import path3 from "path";
416
+ async function generateVirtualProject(config) {
417
+ const vfs = new VirtualFileSystem();
418
+ const errors = [];
419
+ try {
420
+ await processBaseTemplates(vfs, config);
421
+ await processWebAppTemplates(vfs, config);
422
+ await processConvexTemplates(vfs, config);
423
+ await processBetterAuthTemplates(vfs, config);
424
+ if (config.structure === "monorepo") {
425
+ await processMonorepoTemplates(vfs, config);
426
+ if (config.marketingSite === "payload") {
427
+ await processPayloadTemplates(vfs, config);
428
+ } else if (config.marketingSite === "nextjs") {
429
+ await processMarketingTemplates(vfs, config);
430
+ }
431
+ }
432
+ await processIntegrationTemplates(vfs, config);
433
+ await processAddonTemplates(vfs, config);
434
+ await postProcess(vfs, config);
435
+ const tree = vfs.toTree(config);
436
+ return {
437
+ tree,
438
+ success: true
439
+ };
440
+ } catch (error) {
441
+ errors.push(error instanceof Error ? error.message : String(error));
442
+ return {
443
+ tree: vfs.toTree(config),
444
+ success: false,
445
+ errors
446
+ };
447
+ }
448
+ }
449
+ function processTemplatesFromPrefix(vfs, prefix, outputPrefix, config) {
450
+ for (const [templatePath, content] of Object.entries(EMBEDDED_TEMPLATES)) {
451
+ if (!templatePath.startsWith(prefix)) continue;
452
+ if (!shouldIncludeFile(templatePath, config)) continue;
453
+ const relativePath = templatePath.slice(prefix.length);
454
+ const outputPath = path3.join(outputPrefix, relativePath);
455
+ const dir = path3.dirname(outputPath);
456
+ const filename = path3.basename(outputPath);
457
+ const transformedFilename = transformFilename(filename, config);
458
+ const finalPath = path3.join(dir, transformedFilename);
459
+ if (isBinaryFile(filename)) {
460
+ vfs.writeFile(finalPath, Buffer.from(content, "base64"));
461
+ } else {
462
+ const processedContent = processTemplateString(content, config);
463
+ vfs.writeFile(finalPath, processedContent);
464
+ }
465
+ }
466
+ }
467
+ async function processBaseTemplates(vfs, config) {
468
+ processTemplatesFromPrefix(vfs, "base/", "/", config);
469
+ }
470
+ async function processWebAppTemplates(vfs, config) {
471
+ const appPath = config.structure === "monorepo" ? "/apps/web" : "/";
472
+ processTemplatesFromPrefix(vfs, "web/", appPath, config);
473
+ }
474
+ async function processConvexTemplates(vfs, config) {
475
+ const convexPath = config.structure === "monorepo" ? "/packages/backend" : "/";
476
+ processTemplatesFromPrefix(vfs, "convex/", convexPath, config);
477
+ }
478
+ async function processBetterAuthTemplates(vfs, config) {
479
+ const webPath = config.structure === "monorepo" ? "/apps/web" : "/";
480
+ processTemplatesFromPrefix(vfs, "auth/", webPath, config);
481
+ }
482
+ async function processMonorepoTemplates(vfs, config) {
483
+ processTemplatesFromPrefix(vfs, "monorepo/", "/", config);
484
+ processTemplatesFromPrefix(vfs, "packages/ui/", "/packages/ui", config);
485
+ processTemplatesFromPrefix(vfs, "packages/config/", "/packages/config", config);
486
+ }
487
+ async function processPayloadTemplates(vfs, config) {
488
+ processTemplatesFromPrefix(vfs, "marketing/payload/", "/apps/marketing", config);
489
+ }
490
+ async function processMarketingTemplates(vfs, config) {
491
+ processTemplatesFromPrefix(vfs, "marketing/nextjs/", "/apps/marketing", config);
492
+ }
493
+ async function processIntegrationTemplates(vfs, config) {
494
+ const webPath = config.structure === "monorepo" ? "/apps/web" : "/";
495
+ if (config.integrations.analytics === "posthog") {
496
+ processTemplatesFromPrefix(vfs, "integrations/posthog/", webPath, config);
497
+ }
498
+ if (config.integrations.analytics === "vercel") {
499
+ processTemplatesFromPrefix(vfs, "integrations/vercel-analytics/", webPath, config);
500
+ }
501
+ if (config.integrations.uploads === "uploadthing") {
502
+ processTemplatesFromPrefix(vfs, "integrations/uploadthing/", webPath, config);
503
+ }
504
+ if (config.integrations.uploads === "s3") {
505
+ processTemplatesFromPrefix(vfs, "integrations/s3/", webPath, config);
506
+ }
507
+ if (config.integrations.uploads === "vercel-blob") {
508
+ processTemplatesFromPrefix(vfs, "integrations/vercel-blob/", webPath, config);
509
+ }
510
+ }
511
+ async function processAddonTemplates(vfs, config) {
512
+ for (const addon of config.addons) {
513
+ processTemplatesFromPrefix(vfs, `addons/${addon}/`, "/", config);
514
+ }
515
+ }
516
+ async function postProcess(vfs, config) {
517
+ const scriptsPath = config.structure === "monorepo" ? "/scripts" : "/scripts";
518
+ const webScriptsPath = config.structure === "monorepo" ? "/apps/web/scripts" : "/scripts";
519
+ generateDevScript(vfs, scriptsPath, config);
520
+ generateSetupConvexScript(vfs, webScriptsPath, config);
521
+ generateReadme(vfs, config);
522
+ }
523
+ function generateDevScript(vfs, scriptsPath, config) {
524
+ const isMonorepo = config.structure === "monorepo";
525
+ const webAppDir = isMonorepo ? "apps/web" : ".";
526
+ const devScript = `#!/usr/bin/env node
527
+ /**
528
+ * Dev Script - Starts Next.js and Convex dev servers
529
+ */
530
+
531
+ import { spawn, execSync } from 'child_process'
532
+ import { existsSync, readFileSync } from 'fs'
533
+ import { resolve, dirname } from 'path'
534
+ import { fileURLToPath } from 'url'
535
+
536
+ const __dirname = dirname(fileURLToPath(import.meta.url))
537
+ const rootDir = resolve(__dirname, '..')
538
+ const webAppDir = resolve(rootDir, '${webAppDir}')
539
+
540
+ function loadEnvFile(dir) {
541
+ const envPath = resolve(dir, '.env.local')
542
+ if (!existsSync(envPath)) return {}
543
+
544
+ const content = readFileSync(envPath, 'utf-8')
545
+ const env = {}
546
+
547
+ for (const line of content.split('\\n')) {
548
+ const trimmed = line.trim()
549
+ if (!trimmed || trimmed.startsWith('#')) continue
550
+ const eqIndex = trimmed.indexOf('=')
551
+ if (eqIndex === -1) continue
552
+ const key = trimmed.slice(0, eqIndex)
553
+ let value = trimmed.slice(eqIndex + 1)
554
+ if ((value.startsWith('"') && value.endsWith('"')) ||
555
+ (value.startsWith("'") && value.endsWith("'"))) {
556
+ value = value.slice(1, -1)
557
+ }
558
+ if (value) env[key] = value
559
+ }
560
+ return env
561
+ }
562
+
563
+ async function checkAndInstall() {
564
+ if (!existsSync(resolve(rootDir, 'node_modules'))) {
565
+ console.log('\u{1F4E6} Installing dependencies...\\n')
566
+ execSync('pnpm install', { cwd: rootDir, stdio: 'inherit' })
567
+ }
568
+ }
569
+
570
+ function startDevServers() {
571
+ const localEnv = loadEnvFile(webAppDir)
572
+
573
+ if (!localEnv.CONVEX_DEPLOYMENT) {
574
+ console.log('\u26A0\uFE0F Convex not configured. Run: pnpm dev:setup\\n')
575
+ console.log('Starting Next.js only...\\n')
576
+ spawn('pnpm', ['${isMonorepo ? "dev:turbo" : "dev:next"}'], {
577
+ cwd: rootDir, stdio: 'inherit', shell: true
578
+ })
579
+ return
580
+ }
581
+
582
+ console.log('\u{1F680} Starting development servers...\\n')
583
+
584
+ const nextProcess = spawn('pnpm', ['${isMonorepo ? "dev:turbo" : "dev:next"}'], {
585
+ cwd: rootDir, stdio: 'inherit', shell: true
586
+ })
587
+
588
+ const convexProcess = spawn('npx', ['convex', 'dev'], {
589
+ cwd: webAppDir, stdio: 'inherit', shell: true
590
+ })
591
+
592
+ const cleanup = () => {
593
+ nextProcess.kill()
594
+ convexProcess.kill()
595
+ process.exit(0)
596
+ }
597
+
598
+ process.on('SIGINT', cleanup)
599
+ process.on('SIGTERM', cleanup)
600
+ }
601
+
602
+ async function main() {
603
+ await checkAndInstall()
604
+ startDevServers()
605
+ }
606
+
607
+ main()
608
+ `;
609
+ vfs.writeFile(`${scriptsPath}/dev.mjs`, devScript);
610
+ }
611
+ function generateSetupConvexScript(vfs, scriptsPath, config) {
612
+ const setupScript = `#!/usr/bin/env node
613
+ /**
614
+ * Setup Convex - Interactive setup wizard for Convex
615
+ */
616
+
617
+ import { execSync, spawnSync } from 'child_process'
618
+ import { existsSync, readFileSync, writeFileSync } from 'fs'
619
+ import { resolve, dirname } from 'path'
620
+ import { fileURLToPath } from 'url'
621
+ import * as readline from 'readline'
622
+
623
+ const __dirname = dirname(fileURLToPath(import.meta.url))
624
+ const projectDir = resolve(__dirname, '..')
625
+
626
+ function prompt(question) {
627
+ const rl = readline.createInterface({
628
+ input: process.stdin,
629
+ output: process.stdout
630
+ })
631
+
632
+ return new Promise((resolve) => {
633
+ rl.question(question, (answer) => {
634
+ rl.close()
635
+ resolve(answer.trim())
636
+ })
637
+ })
638
+ }
639
+
640
+ async function main() {
641
+ console.log('\\n\u{1F527} Convex Setup Wizard\\n')
642
+
643
+ // Check if already configured
644
+ const envPath = resolve(projectDir, '.env.local')
645
+ if (existsSync(envPath)) {
646
+ const content = readFileSync(envPath, 'utf-8')
647
+ if (content.includes('CONVEX_DEPLOYMENT=') && !content.includes('CONVEX_DEPLOYMENT=\\n')) {
648
+ console.log('\u2705 Convex is already configured!')
649
+ console.log(' Run "pnpm dev" to start development.\\n')
650
+ return
651
+ }
652
+ }
653
+
654
+ console.log('This wizard will help you set up Convex for your project.\\n')
655
+
656
+ // Run convex dev to trigger authentication and project creation
657
+ console.log('Running Convex setup (this will open your browser if needed)...\\n')
658
+
659
+ try {
660
+ spawnSync('npx', ['convex', 'dev', '--once'], {
661
+ cwd: projectDir,
662
+ stdio: 'inherit',
663
+ shell: true
664
+ })
665
+
666
+ console.log('\\n\u2705 Convex setup complete!')
667
+ console.log(' Run "pnpm dev" to start development.\\n')
668
+ } catch (error) {
669
+ console.error('\\n\u274C Convex setup failed:', error.message)
670
+ console.error(' Try running "npx convex dev" manually.\\n')
671
+ process.exit(1)
672
+ }
673
+ }
674
+
675
+ main()
676
+ `;
677
+ vfs.writeFile(`${scriptsPath}/setup-convex.mjs`, setupScript);
678
+ }
679
+ function generateReadme(vfs, config) {
680
+ const readme = `# ${config.projectName}
681
+
682
+ Built with [create-kofi-stack](https://github.com/theodenanyoh11/create-kofi-stack)
683
+
684
+ ## Tech Stack
685
+
686
+ - **Framework**: Next.js 16 with App Router
687
+ - **Backend**: Convex (reactive backend-as-a-service)
688
+ - **Auth**: Better-Auth with Convex adapter
689
+ - **UI**: shadcn/ui with ${config.shadcn.componentLibrary}
690
+ - **Styling**: Tailwind CSS v4
691
+
692
+ ## Getting Started
693
+
694
+ \`\`\`bash
695
+ cd ${config.projectName}
696
+ pnpm dev
697
+ \`\`\`
698
+
699
+ This will:
700
+ - Install dependencies (if needed)
701
+ - Set up Convex (if not configured)
702
+ - Start Next.js and Convex dev servers
703
+
704
+ ## Project Structure
705
+
706
+ ${config.structure === "monorepo" ? `
707
+ \`\`\`
708
+ \u251C\u2500\u2500 apps/
709
+ \u2502 \u251C\u2500\u2500 web/ # Main Next.js application
710
+ \u2502 ${config.marketingSite !== "none" ? "\u251C\u2500\u2500 marketing/ # Marketing site" : ""}
711
+ \u2502 \u2514\u2500\u2500 design-system/ # Component showcase
712
+ \u251C\u2500\u2500 packages/
713
+ \u2502 \u251C\u2500\u2500 backend/ # Convex functions
714
+ \u2502 \u2514\u2500\u2500 ui/ # Shared UI components
715
+ \u2514\u2500\u2500 ...
716
+ \`\`\`
717
+ ` : `
718
+ \`\`\`
719
+ \u251C\u2500\u2500 src/
720
+ \u2502 \u251C\u2500\u2500 app/ # Next.js App Router
721
+ \u2502 \u251C\u2500\u2500 components/ # React components
722
+ \u2502 \u2514\u2500\u2500 lib/ # Utilities
723
+ \u251C\u2500\u2500 convex/ # Convex functions
724
+ \u2514\u2500\u2500 ...
725
+ \`\`\`
726
+ `}
727
+
728
+ ## Documentation
729
+
730
+ - [Convex](https://docs.convex.dev)
731
+ - [Better-Auth](https://www.better-auth.com)
732
+ - [shadcn/ui](https://ui.shadcn.com)
733
+ - [Next.js](https://nextjs.org/docs)
734
+ `;
735
+ vfs.writeFile("/README.md", readme);
736
+ }
737
+ export {
738
+ VirtualFileSystem,
739
+ generateVirtualProject,
740
+ isBinaryFile,
741
+ processTemplateString,
742
+ shouldIncludeFile,
743
+ transformFilename
744
+ };