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.
- package/.turbo/turbo-build.log +20 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +744 -0
- package/package.json +29 -0
- package/scripts/generate-templates.js +104 -0
- package/src/core/index.ts +7 -0
- package/src/core/template-processor.ts +127 -0
- package/src/core/virtual-fs.ts +189 -0
- package/src/generator.ts +429 -0
- package/src/index.ts +19 -0
- package/src/templates.generated.ts +39 -0
- package/templates/base/_gitignore.hbs +45 -0
- package/templates/base/biome.json.hbs +34 -0
- package/templates/convex/_env.local.hbs +52 -0
- package/templates/convex/convex/auth.ts.hbs +7 -0
- package/templates/convex/convex/http.ts.hbs +8 -0
- package/templates/convex/convex/schema.ts.hbs +15 -0
- package/templates/convex/convex/users.ts.hbs +13 -0
- package/templates/integrations/posthog/src/components/providers/posthog-provider.tsx.hbs +17 -0
- package/templates/monorepo/package.json.hbs +29 -0
- package/templates/monorepo/pnpm-workspace.yaml.hbs +3 -0
- package/templates/monorepo/turbo.json.hbs +42 -0
- package/templates/packages/config-biome/biome.json.hbs +4 -0
- package/templates/packages/config-biome/package.json.hbs +6 -0
- package/templates/packages/config-typescript/base.json.hbs +17 -0
- package/templates/packages/config-typescript/nextjs.json.hbs +7 -0
- package/templates/packages/config-typescript/package.json.hbs +10 -0
- package/templates/packages/ui/components.json.hbs +20 -0
- package/templates/packages/ui/package.json.hbs +34 -0
- package/templates/packages/ui/src/index.ts.hbs +3 -0
- package/templates/packages/ui/src/lib/utils.ts.hbs +6 -0
- package/templates/packages/ui/tsconfig.json.hbs +22 -0
- package/templates/web/components.json.hbs +20 -0
- package/templates/web/next.config.ts.hbs +9 -0
- package/templates/web/package.json.hbs +62 -0
- package/templates/web/postcss.config.mjs.hbs +5 -0
- package/templates/web/src/app/globals.css.hbs +122 -0
- package/templates/web/src/app/layout.tsx.hbs +55 -0
- package/templates/web/src/app/page.tsx.hbs +74 -0
- package/templates/web/src/components/providers/convex-provider.tsx.hbs +18 -0
- package/templates/web/src/lib/auth.ts.hbs +23 -0
- package/templates/web/src/lib/utils.ts.hbs +6 -0
- package/templates/web/tsconfig.json.hbs +23 -0
- package/tsconfig.json +15 -0
package/src/generator.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectConfig,
|
|
3
|
+
VirtualFileTree,
|
|
4
|
+
GeneratorResult,
|
|
5
|
+
} from 'kofi-stack-types'
|
|
6
|
+
import { VirtualFileSystem } from './core/virtual-fs.js'
|
|
7
|
+
import {
|
|
8
|
+
processTemplateString,
|
|
9
|
+
transformFilename,
|
|
10
|
+
isBinaryFile,
|
|
11
|
+
shouldIncludeFile,
|
|
12
|
+
} from './core/template-processor.js'
|
|
13
|
+
import { EMBEDDED_TEMPLATES } from './templates.generated.js'
|
|
14
|
+
import path from 'path'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate a virtual project based on the provided configuration.
|
|
18
|
+
* Returns a VirtualFileTree that can be written to disk.
|
|
19
|
+
*/
|
|
20
|
+
export async function generateVirtualProject(
|
|
21
|
+
config: ProjectConfig
|
|
22
|
+
): Promise<GeneratorResult> {
|
|
23
|
+
const vfs = new VirtualFileSystem()
|
|
24
|
+
const errors: string[] = []
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Process templates in order
|
|
28
|
+
await processBaseTemplates(vfs, config)
|
|
29
|
+
await processWebAppTemplates(vfs, config)
|
|
30
|
+
await processConvexTemplates(vfs, config)
|
|
31
|
+
await processBetterAuthTemplates(vfs, config)
|
|
32
|
+
|
|
33
|
+
if (config.structure === 'monorepo') {
|
|
34
|
+
await processMonorepoTemplates(vfs, config)
|
|
35
|
+
if (config.marketingSite === 'payload') {
|
|
36
|
+
await processPayloadTemplates(vfs, config)
|
|
37
|
+
} else if (config.marketingSite === 'nextjs') {
|
|
38
|
+
await processMarketingTemplates(vfs, config)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
await processIntegrationTemplates(vfs, config)
|
|
43
|
+
await processAddonTemplates(vfs, config)
|
|
44
|
+
|
|
45
|
+
// Post-processing: Generate package.json, README, etc.
|
|
46
|
+
await postProcess(vfs, config)
|
|
47
|
+
|
|
48
|
+
const tree = vfs.toTree(config)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
tree,
|
|
52
|
+
success: true,
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
errors.push(error instanceof Error ? error.message : String(error))
|
|
56
|
+
return {
|
|
57
|
+
tree: vfs.toTree(config),
|
|
58
|
+
success: false,
|
|
59
|
+
errors,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Process templates from a given prefix path
|
|
66
|
+
*/
|
|
67
|
+
function processTemplatesFromPrefix(
|
|
68
|
+
vfs: VirtualFileSystem,
|
|
69
|
+
prefix: string,
|
|
70
|
+
outputPrefix: string,
|
|
71
|
+
config: ProjectConfig
|
|
72
|
+
): void {
|
|
73
|
+
for (const [templatePath, content] of Object.entries(EMBEDDED_TEMPLATES)) {
|
|
74
|
+
if (!templatePath.startsWith(prefix)) continue
|
|
75
|
+
if (!shouldIncludeFile(templatePath, config)) continue
|
|
76
|
+
|
|
77
|
+
// Calculate output path
|
|
78
|
+
const relativePath = templatePath.slice(prefix.length)
|
|
79
|
+
const outputPath = path.join(outputPrefix, relativePath)
|
|
80
|
+
|
|
81
|
+
// Transform filename
|
|
82
|
+
const dir = path.dirname(outputPath)
|
|
83
|
+
const filename = path.basename(outputPath)
|
|
84
|
+
const transformedFilename = transformFilename(filename, config)
|
|
85
|
+
const finalPath = path.join(dir, transformedFilename)
|
|
86
|
+
|
|
87
|
+
// Process content
|
|
88
|
+
if (isBinaryFile(filename)) {
|
|
89
|
+
// For binary files, we store the source path for later copying
|
|
90
|
+
vfs.writeFile(finalPath, Buffer.from(content, 'base64'))
|
|
91
|
+
} else {
|
|
92
|
+
// Process as template
|
|
93
|
+
const processedContent = processTemplateString(content, config)
|
|
94
|
+
vfs.writeFile(finalPath, processedContent)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function processBaseTemplates(
|
|
100
|
+
vfs: VirtualFileSystem,
|
|
101
|
+
config: ProjectConfig
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
processTemplatesFromPrefix(vfs, 'base/', '/', config)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function processWebAppTemplates(
|
|
107
|
+
vfs: VirtualFileSystem,
|
|
108
|
+
config: ProjectConfig
|
|
109
|
+
): Promise<void> {
|
|
110
|
+
const appPath = config.structure === 'monorepo' ? '/apps/web' : '/'
|
|
111
|
+
processTemplatesFromPrefix(vfs, 'web/', appPath, config)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function processConvexTemplates(
|
|
115
|
+
vfs: VirtualFileSystem,
|
|
116
|
+
config: ProjectConfig
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
const convexPath = config.structure === 'monorepo' ? '/packages/backend' : '/'
|
|
119
|
+
processTemplatesFromPrefix(vfs, 'convex/', convexPath, config)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function processBetterAuthTemplates(
|
|
123
|
+
vfs: VirtualFileSystem,
|
|
124
|
+
config: ProjectConfig
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const webPath = config.structure === 'monorepo' ? '/apps/web' : '/'
|
|
127
|
+
processTemplatesFromPrefix(vfs, 'auth/', webPath, config)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function processMonorepoTemplates(
|
|
131
|
+
vfs: VirtualFileSystem,
|
|
132
|
+
config: ProjectConfig
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
processTemplatesFromPrefix(vfs, 'monorepo/', '/', config)
|
|
135
|
+
processTemplatesFromPrefix(vfs, 'packages/ui/', '/packages/ui', config)
|
|
136
|
+
processTemplatesFromPrefix(vfs, 'packages/config/', '/packages/config', config)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function processPayloadTemplates(
|
|
140
|
+
vfs: VirtualFileSystem,
|
|
141
|
+
config: ProjectConfig
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
processTemplatesFromPrefix(vfs, 'marketing/payload/', '/apps/marketing', config)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function processMarketingTemplates(
|
|
147
|
+
vfs: VirtualFileSystem,
|
|
148
|
+
config: ProjectConfig
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
processTemplatesFromPrefix(vfs, 'marketing/nextjs/', '/apps/marketing', config)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function processIntegrationTemplates(
|
|
154
|
+
vfs: VirtualFileSystem,
|
|
155
|
+
config: ProjectConfig
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
const webPath = config.structure === 'monorepo' ? '/apps/web' : '/'
|
|
158
|
+
|
|
159
|
+
if (config.integrations.analytics === 'posthog') {
|
|
160
|
+
processTemplatesFromPrefix(vfs, 'integrations/posthog/', webPath, config)
|
|
161
|
+
}
|
|
162
|
+
if (config.integrations.analytics === 'vercel') {
|
|
163
|
+
processTemplatesFromPrefix(vfs, 'integrations/vercel-analytics/', webPath, config)
|
|
164
|
+
}
|
|
165
|
+
if (config.integrations.uploads === 'uploadthing') {
|
|
166
|
+
processTemplatesFromPrefix(vfs, 'integrations/uploadthing/', webPath, config)
|
|
167
|
+
}
|
|
168
|
+
if (config.integrations.uploads === 's3') {
|
|
169
|
+
processTemplatesFromPrefix(vfs, 'integrations/s3/', webPath, config)
|
|
170
|
+
}
|
|
171
|
+
if (config.integrations.uploads === 'vercel-blob') {
|
|
172
|
+
processTemplatesFromPrefix(vfs, 'integrations/vercel-blob/', webPath, config)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function processAddonTemplates(
|
|
177
|
+
vfs: VirtualFileSystem,
|
|
178
|
+
config: ProjectConfig
|
|
179
|
+
): Promise<void> {
|
|
180
|
+
for (const addon of config.addons) {
|
|
181
|
+
processTemplatesFromPrefix(vfs, `addons/${addon}/`, '/', config)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function postProcess(
|
|
186
|
+
vfs: VirtualFileSystem,
|
|
187
|
+
config: ProjectConfig
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
// Generate scripts directory with dev.mjs and setup scripts
|
|
190
|
+
const scriptsPath = config.structure === 'monorepo' ? '/scripts' : '/scripts'
|
|
191
|
+
const webScriptsPath = config.structure === 'monorepo' ? '/apps/web/scripts' : '/scripts'
|
|
192
|
+
|
|
193
|
+
generateDevScript(vfs, scriptsPath, config)
|
|
194
|
+
generateSetupConvexScript(vfs, webScriptsPath, config)
|
|
195
|
+
|
|
196
|
+
// Generate README
|
|
197
|
+
generateReadme(vfs, config)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function generateDevScript(
|
|
201
|
+
vfs: VirtualFileSystem,
|
|
202
|
+
scriptsPath: string,
|
|
203
|
+
config: ProjectConfig
|
|
204
|
+
): void {
|
|
205
|
+
const isMonorepo = config.structure === 'monorepo'
|
|
206
|
+
const webAppDir = isMonorepo ? 'apps/web' : '.'
|
|
207
|
+
|
|
208
|
+
const devScript = `#!/usr/bin/env node
|
|
209
|
+
/**
|
|
210
|
+
* Dev Script - Starts Next.js and Convex dev servers
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
import { spawn, execSync } from 'child_process'
|
|
214
|
+
import { existsSync, readFileSync } from 'fs'
|
|
215
|
+
import { resolve, dirname } from 'path'
|
|
216
|
+
import { fileURLToPath } from 'url'
|
|
217
|
+
|
|
218
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
219
|
+
const rootDir = resolve(__dirname, '..')
|
|
220
|
+
const webAppDir = resolve(rootDir, '${webAppDir}')
|
|
221
|
+
|
|
222
|
+
function loadEnvFile(dir) {
|
|
223
|
+
const envPath = resolve(dir, '.env.local')
|
|
224
|
+
if (!existsSync(envPath)) return {}
|
|
225
|
+
|
|
226
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
227
|
+
const env = {}
|
|
228
|
+
|
|
229
|
+
for (const line of content.split('\\n')) {
|
|
230
|
+
const trimmed = line.trim()
|
|
231
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
232
|
+
const eqIndex = trimmed.indexOf('=')
|
|
233
|
+
if (eqIndex === -1) continue
|
|
234
|
+
const key = trimmed.slice(0, eqIndex)
|
|
235
|
+
let value = trimmed.slice(eqIndex + 1)
|
|
236
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
237
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
238
|
+
value = value.slice(1, -1)
|
|
239
|
+
}
|
|
240
|
+
if (value) env[key] = value
|
|
241
|
+
}
|
|
242
|
+
return env
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function checkAndInstall() {
|
|
246
|
+
if (!existsSync(resolve(rootDir, 'node_modules'))) {
|
|
247
|
+
console.log('📦 Installing dependencies...\\n')
|
|
248
|
+
execSync('pnpm install', { cwd: rootDir, stdio: 'inherit' })
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function startDevServers() {
|
|
253
|
+
const localEnv = loadEnvFile(webAppDir)
|
|
254
|
+
|
|
255
|
+
if (!localEnv.CONVEX_DEPLOYMENT) {
|
|
256
|
+
console.log('⚠️ Convex not configured. Run: pnpm dev:setup\\n')
|
|
257
|
+
console.log('Starting Next.js only...\\n')
|
|
258
|
+
spawn('pnpm', ['${isMonorepo ? 'dev:turbo' : 'dev:next'}'], {
|
|
259
|
+
cwd: rootDir, stdio: 'inherit', shell: true
|
|
260
|
+
})
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log('🚀 Starting development servers...\\n')
|
|
265
|
+
|
|
266
|
+
const nextProcess = spawn('pnpm', ['${isMonorepo ? 'dev:turbo' : 'dev:next'}'], {
|
|
267
|
+
cwd: rootDir, stdio: 'inherit', shell: true
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
const convexProcess = spawn('npx', ['convex', 'dev'], {
|
|
271
|
+
cwd: webAppDir, stdio: 'inherit', shell: true
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const cleanup = () => {
|
|
275
|
+
nextProcess.kill()
|
|
276
|
+
convexProcess.kill()
|
|
277
|
+
process.exit(0)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
process.on('SIGINT', cleanup)
|
|
281
|
+
process.on('SIGTERM', cleanup)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function main() {
|
|
285
|
+
await checkAndInstall()
|
|
286
|
+
startDevServers()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
main()
|
|
290
|
+
`
|
|
291
|
+
|
|
292
|
+
vfs.writeFile(`${scriptsPath}/dev.mjs`, devScript)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function generateSetupConvexScript(
|
|
296
|
+
vfs: VirtualFileSystem,
|
|
297
|
+
scriptsPath: string,
|
|
298
|
+
config: ProjectConfig
|
|
299
|
+
): void {
|
|
300
|
+
const setupScript = `#!/usr/bin/env node
|
|
301
|
+
/**
|
|
302
|
+
* Setup Convex - Interactive setup wizard for Convex
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
import { execSync, spawnSync } from 'child_process'
|
|
306
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
307
|
+
import { resolve, dirname } from 'path'
|
|
308
|
+
import { fileURLToPath } from 'url'
|
|
309
|
+
import * as readline from 'readline'
|
|
310
|
+
|
|
311
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
312
|
+
const projectDir = resolve(__dirname, '..')
|
|
313
|
+
|
|
314
|
+
function prompt(question) {
|
|
315
|
+
const rl = readline.createInterface({
|
|
316
|
+
input: process.stdin,
|
|
317
|
+
output: process.stdout
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return new Promise((resolve) => {
|
|
321
|
+
rl.question(question, (answer) => {
|
|
322
|
+
rl.close()
|
|
323
|
+
resolve(answer.trim())
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function main() {
|
|
329
|
+
console.log('\\n🔧 Convex Setup Wizard\\n')
|
|
330
|
+
|
|
331
|
+
// Check if already configured
|
|
332
|
+
const envPath = resolve(projectDir, '.env.local')
|
|
333
|
+
if (existsSync(envPath)) {
|
|
334
|
+
const content = readFileSync(envPath, 'utf-8')
|
|
335
|
+
if (content.includes('CONVEX_DEPLOYMENT=') && !content.includes('CONVEX_DEPLOYMENT=\\n')) {
|
|
336
|
+
console.log('✅ Convex is already configured!')
|
|
337
|
+
console.log(' Run "pnpm dev" to start development.\\n')
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
console.log('This wizard will help you set up Convex for your project.\\n')
|
|
343
|
+
|
|
344
|
+
// Run convex dev to trigger authentication and project creation
|
|
345
|
+
console.log('Running Convex setup (this will open your browser if needed)...\\n')
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
spawnSync('npx', ['convex', 'dev', '--once'], {
|
|
349
|
+
cwd: projectDir,
|
|
350
|
+
stdio: 'inherit',
|
|
351
|
+
shell: true
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
console.log('\\n✅ Convex setup complete!')
|
|
355
|
+
console.log(' Run "pnpm dev" to start development.\\n')
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('\\n❌ Convex setup failed:', error.message)
|
|
358
|
+
console.error(' Try running "npx convex dev" manually.\\n')
|
|
359
|
+
process.exit(1)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
main()
|
|
364
|
+
`
|
|
365
|
+
|
|
366
|
+
vfs.writeFile(`${scriptsPath}/setup-convex.mjs`, setupScript)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function generateReadme(vfs: VirtualFileSystem, config: ProjectConfig): void {
|
|
370
|
+
const readme = `# ${config.projectName}
|
|
371
|
+
|
|
372
|
+
Built with [create-kofi-stack](https://github.com/theodenanyoh11/create-kofi-stack)
|
|
373
|
+
|
|
374
|
+
## Tech Stack
|
|
375
|
+
|
|
376
|
+
- **Framework**: Next.js 16 with App Router
|
|
377
|
+
- **Backend**: Convex (reactive backend-as-a-service)
|
|
378
|
+
- **Auth**: Better-Auth with Convex adapter
|
|
379
|
+
- **UI**: shadcn/ui with ${config.shadcn.componentLibrary}
|
|
380
|
+
- **Styling**: Tailwind CSS v4
|
|
381
|
+
|
|
382
|
+
## Getting Started
|
|
383
|
+
|
|
384
|
+
\`\`\`bash
|
|
385
|
+
cd ${config.projectName}
|
|
386
|
+
pnpm dev
|
|
387
|
+
\`\`\`
|
|
388
|
+
|
|
389
|
+
This will:
|
|
390
|
+
- Install dependencies (if needed)
|
|
391
|
+
- Set up Convex (if not configured)
|
|
392
|
+
- Start Next.js and Convex dev servers
|
|
393
|
+
|
|
394
|
+
## Project Structure
|
|
395
|
+
|
|
396
|
+
${config.structure === 'monorepo' ? `
|
|
397
|
+
\`\`\`
|
|
398
|
+
├── apps/
|
|
399
|
+
│ ├── web/ # Main Next.js application
|
|
400
|
+
│ ${config.marketingSite !== 'none' ? '├── marketing/ # Marketing site' : ''}
|
|
401
|
+
│ └── design-system/ # Component showcase
|
|
402
|
+
├── packages/
|
|
403
|
+
│ ├── backend/ # Convex functions
|
|
404
|
+
│ └── ui/ # Shared UI components
|
|
405
|
+
└── ...
|
|
406
|
+
\`\`\`
|
|
407
|
+
` : `
|
|
408
|
+
\`\`\`
|
|
409
|
+
├── src/
|
|
410
|
+
│ ├── app/ # Next.js App Router
|
|
411
|
+
│ ├── components/ # React components
|
|
412
|
+
│ └── lib/ # Utilities
|
|
413
|
+
├── convex/ # Convex functions
|
|
414
|
+
└── ...
|
|
415
|
+
\`\`\`
|
|
416
|
+
`}
|
|
417
|
+
|
|
418
|
+
## Documentation
|
|
419
|
+
|
|
420
|
+
- [Convex](https://docs.convex.dev)
|
|
421
|
+
- [Better-Auth](https://www.better-auth.com)
|
|
422
|
+
- [shadcn/ui](https://ui.shadcn.com)
|
|
423
|
+
- [Next.js](https://nextjs.org/docs)
|
|
424
|
+
`
|
|
425
|
+
|
|
426
|
+
vfs.writeFile('/README.md', readme)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export { VirtualFileSystem }
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Main generator
|
|
2
|
+
export { generateVirtualProject, VirtualFileSystem } from './generator.js'
|
|
3
|
+
|
|
4
|
+
// Core utilities
|
|
5
|
+
export {
|
|
6
|
+
processTemplateString,
|
|
7
|
+
transformFilename,
|
|
8
|
+
isBinaryFile,
|
|
9
|
+
shouldIncludeFile,
|
|
10
|
+
} from './core/template-processor.js'
|
|
11
|
+
|
|
12
|
+
// Re-export types
|
|
13
|
+
export type {
|
|
14
|
+
ProjectConfig,
|
|
15
|
+
VirtualFileTree,
|
|
16
|
+
VirtualFile,
|
|
17
|
+
VirtualDirectory,
|
|
18
|
+
GeneratorResult,
|
|
19
|
+
} from 'kofi-stack-types'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Auto-generated file. Do not edit manually.
|
|
2
|
+
// Run 'pnpm prebuild' to regenerate.
|
|
3
|
+
// Generated: 2026-01-13T20:11:02.957Z
|
|
4
|
+
// Template count: 32
|
|
5
|
+
|
|
6
|
+
export const EMBEDDED_TEMPLATES: Record<string, string> = {
|
|
7
|
+
"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",
|
|
8
|
+
"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",
|
|
9
|
+
"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",
|
|
10
|
+
"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",
|
|
11
|
+
"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",
|
|
12
|
+
"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",
|
|
13
|
+
"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",
|
|
14
|
+
"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",
|
|
15
|
+
"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",
|
|
16
|
+
"monorepo/pnpm-workspace.yaml.hbs": "packages:\n - \"apps/*\"\n - \"packages/*\"\n",
|
|
17
|
+
"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",
|
|
18
|
+
"packages/config-biome/biome.json.hbs": "{\n \"$schema\": \"https://biomejs.dev/schemas/1.9.4/schema.json\",\n \"extends\": [\"../../biome.json\"]\n}\n",
|
|
19
|
+
"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",
|
|
20
|
+
"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",
|
|
21
|
+
"packages/config-typescript/nextjs.json.hbs": "{\n \"extends\": \"./base.json\",\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"plugins\": [{ \"name\": \"next\" }]\n }\n}\n",
|
|
22
|
+
"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",
|
|
23
|
+
"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",
|
|
24
|
+
"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",
|
|
25
|
+
"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",
|
|
26
|
+
"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",
|
|
27
|
+
"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",
|
|
28
|
+
"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",
|
|
29
|
+
"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",
|
|
30
|
+
"web/package.json.hbs": "{\n \"name\": \"{{#if (eq structure 'monorepo')}}@repo/web{{else}}{{projectName}}{{/if}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"{{#if (eq structure 'monorepo')}}next dev --turbopack{{else}}node scripts/dev.mjs{{/if}}\",\n \"dev:next\": \"next dev --turbopack\",\n \"build\": \"next build\",\n \"start\": \"next start\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:e2e\": \"playwright test\",\n \"setup:convex\": \"node scripts/setup-convex.mjs\"\n },\n \"dependencies\": {\n \"next\": \"^16.0.0\",\n \"react\": \"^19.0.0\",\n \"react-dom\": \"^19.0.0\",\n \"convex\": \"^1.25.0\",\n \"@convex-dev/auth\": \"^0.0.90\",\n \"@auth/core\": \"^0.37.0\",\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 \"tw-animate-css\": \"^1.3.0\",\n \"resend\": \"^4.0.0\",\n \"react-email\": \"^3.0.0\",\n \"@react-email/components\": \"^0.0.36\"{{#if (eq integrations.analytics 'posthog')}},\n \"posthog-js\": \"^1.200.0\",\n \"posthog-node\": \"^5.0.0\"{{/if}}{{#if (eq integrations.analytics 'vercel')}},\n \"@vercel/analytics\": \"^1.4.0\",\n \"@vercel/speed-insights\": \"^1.1.0\"{{/if}}{{#if (eq integrations.uploads 'uploadthing')}},\n \"uploadthing\": \"^7.0.0\",\n \"@uploadthing/react\": \"^7.0.0\"{{/if}}{{#if (eq integrations.uploads 's3')}},\n \"@aws-sdk/client-s3\": \"^3.700.0\",\n \"@aws-sdk/s3-request-presigner\": \"^3.700.0\"{{/if}}{{#if (eq integrations.uploads 'vercel-blob')}},\n \"@vercel/blob\": \"^2.0.0\"{{/if}}{{#if (includes addons 'rate-limiting')}},\n \"@arcjet/next\": \"^1.0.0-beta.16\"{{/if}}{{#if (includes addons 'monitoring')}},\n \"@sentry/nextjs\": \"^8.0.0\"{{/if}}\n },\n \"devDependencies\": {\n{{#if (eq structure 'monorepo')}} \"@repo/config-typescript\": \"workspace:*\",\n{{/if}} \"@types/node\": \"^20.0.0\",\n \"@types/react\": \"^19.0.0\",\n \"@types/react-dom\": \"^19.0.0\",\n \"tailwindcss\": \"^4.0.0\",\n \"@tailwindcss/postcss\": \"^4.0.0\",\n \"postcss\": \"^8.4.0\",\n \"typescript\": \"^5.0.0\",\n \"vitest\": \"^3.0.0\",\n \"@vitejs/plugin-react\": \"^4.3.0\",\n \"@testing-library/react\": \"^16.0.0\",\n \"jsdom\": \"^26.0.0\",\n \"playwright\": \"^1.50.0\",\n \"@playwright/test\": \"^1.50.0\"\n }\n}\n",
|
|
31
|
+
"web/postcss.config.mjs.hbs": "export default {\n plugins: {\n '@tailwindcss/postcss': {},\n },\n}\n",
|
|
32
|
+
"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",
|
|
33
|
+
"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",
|
|
34
|
+
"web/src/app/page.tsx.hbs": "'use client'\n\nimport { useAuth } from '@/lib/auth'\n\nexport default function HomePage() {\n const { isAuthenticated, isLoading, signIn, signOut, user } = useAuth()\n\n if (isLoading) {\n return (\n <main className=\"min-h-screen flex items-center justify-center\">\n <div className=\"animate-pulse\">Loading...</div>\n </main>\n )\n }\n\n return (\n <main className=\"min-h-screen flex flex-col items-center justify-center p-8\">\n <div className=\"max-w-2xl text-center space-y-8\">\n <h1 className=\"text-4xl font-bold\">{{projectName}}</h1>\n <p className=\"text-xl text-muted-foreground\">\n Built with Next.js, Convex, Better-Auth, and shadcn/ui\n </p>\n\n {isAuthenticated ? (\n <div className=\"space-y-4\">\n <p className=\"text-lg\">\n Welcome, <span className=\"font-semibold\">{user?.name || user?.email}</span>!\n </p>\n <button\n onClick={() => signOut()}\n className=\"px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition\"\n >\n Sign Out\n </button>\n </div>\n ) : (\n <div className=\"space-y-4\">\n <p className=\"text-muted-foreground\">\n Sign in to get started\n </p>\n <div className=\"flex gap-4 justify-center\">\n <button\n onClick={() => signIn('github')}\n className=\"px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:opacity-90 transition\"\n >\n Sign in with GitHub\n </button>\n <button\n onClick={() => signIn('google')}\n className=\"px-6 py-2 bg-secondary text-secondary-foreground rounded-lg hover:opacity-90 transition\"\n >\n Sign in with Google\n </button>\n </div>\n </div>\n )}\n\n <div className=\"pt-8 border-t border-border\">\n <p className=\"text-sm text-muted-foreground\">\n Created with{' '}\n <a\n href=\"https://github.com/theodenanyoh11/create-kofi-stack\"\n className=\"text-primary hover:underline\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n create-kofi-stack\n </a>\n </p>\n </div>\n </div>\n </main>\n )\n}\n",
|
|
35
|
+
"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",
|
|
36
|
+
"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",
|
|
37
|
+
"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",
|
|
38
|
+
"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"
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules
|
|
3
|
+
.pnpm-store
|
|
4
|
+
|
|
5
|
+
# Build outputs
|
|
6
|
+
.next
|
|
7
|
+
dist
|
|
8
|
+
.turbo
|
|
9
|
+
out
|
|
10
|
+
|
|
11
|
+
# Testing
|
|
12
|
+
coverage
|
|
13
|
+
playwright-report
|
|
14
|
+
test-results
|
|
15
|
+
|
|
16
|
+
# Environment
|
|
17
|
+
.env
|
|
18
|
+
.env.local
|
|
19
|
+
.env.*.local
|
|
20
|
+
|
|
21
|
+
# IDE
|
|
22
|
+
.idea
|
|
23
|
+
.vscode
|
|
24
|
+
*.swp
|
|
25
|
+
*.swo
|
|
26
|
+
.DS_Store
|
|
27
|
+
|
|
28
|
+
# Convex
|
|
29
|
+
.convex
|
|
30
|
+
|
|
31
|
+
# Vercel
|
|
32
|
+
.vercel
|
|
33
|
+
|
|
34
|
+
# Debug
|
|
35
|
+
npm-debug.log*
|
|
36
|
+
yarn-debug.log*
|
|
37
|
+
yarn-error.log*
|
|
38
|
+
.pnpm-debug.log*
|
|
39
|
+
|
|
40
|
+
# TypeScript
|
|
41
|
+
*.tsbuildinfo
|
|
42
|
+
|
|
43
|
+
# Misc
|
|
44
|
+
*.pem
|
|
45
|
+
.cache
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"organizeImports": {
|
|
4
|
+
"enabled": true
|
|
5
|
+
},
|
|
6
|
+
"linter": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"rules": {
|
|
9
|
+
"recommended": true
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "space",
|
|
15
|
+
"indentWidth": 2
|
|
16
|
+
},
|
|
17
|
+
"javascript": {
|
|
18
|
+
"formatter": {
|
|
19
|
+
"quoteStyle": "single",
|
|
20
|
+
"semicolons": "asNeeded"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": {
|
|
24
|
+
"ignore": [
|
|
25
|
+
"node_modules",
|
|
26
|
+
".next",
|
|
27
|
+
"dist",
|
|
28
|
+
".turbo",
|
|
29
|
+
"coverage",
|
|
30
|
+
".vercel",
|
|
31
|
+
"_generated"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Convex
|
|
2
|
+
CONVEX_DEPLOYMENT=
|
|
3
|
+
NEXT_PUBLIC_CONVEX_URL=
|
|
4
|
+
|
|
5
|
+
# Auth - GitHub OAuth
|
|
6
|
+
AUTH_GITHUB_ID=
|
|
7
|
+
AUTH_GITHUB_SECRET=
|
|
8
|
+
|
|
9
|
+
# Auth - Google OAuth
|
|
10
|
+
AUTH_GOOGLE_ID=
|
|
11
|
+
AUTH_GOOGLE_SECRET=
|
|
12
|
+
|
|
13
|
+
# Better Auth Secret (generate with: openssl rand -base64 32)
|
|
14
|
+
BETTER_AUTH_SECRET=
|
|
15
|
+
{{#if (eq integrations.analytics 'posthog')}}
|
|
16
|
+
|
|
17
|
+
# PostHog
|
|
18
|
+
NEXT_PUBLIC_POSTHOG_KEY=
|
|
19
|
+
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{#if (eq integrations.uploads 'uploadthing')}}
|
|
22
|
+
|
|
23
|
+
# UploadThing
|
|
24
|
+
UPLOADTHING_TOKEN=
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{#if (eq integrations.uploads 's3')}}
|
|
27
|
+
|
|
28
|
+
# AWS S3
|
|
29
|
+
AWS_ACCESS_KEY_ID=
|
|
30
|
+
AWS_SECRET_ACCESS_KEY=
|
|
31
|
+
AWS_REGION=
|
|
32
|
+
AWS_S3_BUCKET=
|
|
33
|
+
{{/if}}
|
|
34
|
+
{{#if (eq integrations.uploads 'vercel-blob')}}
|
|
35
|
+
|
|
36
|
+
# Vercel Blob
|
|
37
|
+
BLOB_READ_WRITE_TOKEN=
|
|
38
|
+
{{/if}}
|
|
39
|
+
{{#if (includes addons 'rate-limiting')}}
|
|
40
|
+
|
|
41
|
+
# Arcjet
|
|
42
|
+
ARCJET_KEY=
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{#if (includes addons 'monitoring')}}
|
|
45
|
+
|
|
46
|
+
# Sentry
|
|
47
|
+
SENTRY_DSN=
|
|
48
|
+
SENTRY_AUTH_TOKEN=
|
|
49
|
+
{{/if}}
|
|
50
|
+
|
|
51
|
+
# Email (Resend)
|
|
52
|
+
RESEND_API_KEY=
|