create-raiko 0.0.0-stage → 1.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.
@@ -0,0 +1,439 @@
1
+ // Files shared by every frontend: config, database, auth, email, i18n, deploy.
2
+ export const V = {
3
+ raiko: '^1.0.0', typescript: '5.6.3', bunTypes: '1.1.13', nodeTypes: '22.9.0', tsx: '4.19.2', esbuild: '0.24.0', vite: '5.4.11', terser: '5.36.0',
4
+ kastor: '0.2.0', prisma: '5.22.0', drizzle: '0.38.3', drizzleKit: '0.30.1', betterSqlite: '11.7.0', pg: '8.13.1', postgresJs: '3.4.5',
5
+ glyphfox: '1.3.0', lucide: '0.469.0', heroicons: '2.2.0', motion: '11.15.0', nodemailer: '6.9.16',
6
+ }
7
+ export const LOCALES = { en: 'English', 'pt-BR': 'Portugues', es: 'Espanol', fr: 'Francais', de: 'Deutsch' }
8
+ const T = {
9
+ en: { title: 'Welcome to Raiko', subtitle: 'Edit app/page.tsx and save to reload.', docs: 'Read the docs', count: 'Clicked {count} times' },
10
+ 'pt-BR': { title: 'Bem vindo ao Raiko', subtitle: 'Edite app/page.tsx e salve para recarregar.', docs: 'Ler a documentacao', count: 'Clicou {count} vezes' },
11
+ es: { title: 'Bienvenido a Raiko', subtitle: 'Edita app/page.tsx y guarda para recargar.', docs: 'Leer la documentacion', count: 'Pulsado {count} veces' },
12
+ fr: { title: 'Bienvenue sur Raiko', subtitle: 'Modifiez app/page.tsx et enregistrez pour recharger.', docs: 'Lire la documentation', count: 'Clique {count} fois' },
13
+ de: { title: 'Willkommen bei Raiko', subtitle: 'Bearbeite app/page.tsx und speichere zum Neuladen.', docs: 'Dokumentation lesen', count: '{count} mal geklickt' },
14
+ }
15
+
16
+ export const isVite = (o) => o.frontend !== 'raiko'
17
+ export const run = (o, s) => (o.pm === 'npm' ? `npm run ${s}` : `${o.pm} ${o.pm === 'yarn' ? '' : 'run '}${s}`.replace(' ', ' '))
18
+ export const exec = (o, s) => (o.pm === 'bun' ? `bunx ${s}` : o.pm === 'pnpm' ? `pnpm dlx ${s}` : `npx ${s}`)
19
+ const json = (v) => JSON.stringify(v, null, 2) + '\n'
20
+
21
+ export function dbFiles(o) {
22
+ const f = {}
23
+ const sqlite = o.db === 'sqlite'
24
+ if (o.orm === 'kastor') {
25
+ f['db/schema.ts'] = `import { table, t } from 'kastor-orm'
26
+
27
+ export const users = table('users', {
28
+ id: t.id(),
29
+ email: t.string(255).unique(),
30
+ name: t.string().nullable(),
31
+ role: t.enum([${Object.keys(o.roles).map((r) => `'${r}'`).join(', ')}, 'user']).default('user'),
32
+ passwordHash: t.string().nullable(),
33
+ totpSecret: t.string().nullable(),
34
+ createdAt: t.datetime().defaultNow(),
35
+ })
36
+
37
+ export const posts = table('posts', {
38
+ id: t.id(),
39
+ title: t.string(200),
40
+ body: t.text().nullable(),
41
+ published: t.boolean().default(false),
42
+ createdAt: t.datetime().defaultNow(),
43
+ })
44
+
45
+ export const schema = { users, posts }
46
+ `
47
+ f['db/index.ts'] = sqlite
48
+ ? `import { kastor } from 'kastor-orm'
49
+ import { sqlite } from 'kastor-orm/sqlite'
50
+ import { schema } from './schema'
51
+
52
+ // bun:sqlite on Bun, better-sqlite3 on Node
53
+ const Database = (globalThis as any).Bun ? (await import('bun:sqlite' as string)).Database : (await import('better-sqlite3' as string)).default
54
+ export const db = kastor({ adapter: sqlite(new Database(process.env.DATABASE_URL ?? 'app.db')), schema })
55
+ `
56
+ : `import pg from 'pg'
57
+ import { kastor } from 'kastor-orm'
58
+ import { postgres } from 'kastor-orm/postgres'
59
+ import { schema } from './schema'
60
+
61
+ export const db = kastor({ adapter: postgres(new pg.Pool({ connectionString: process.env.DATABASE_URL })), schema })
62
+ `
63
+ }
64
+ if (o.orm === 'prisma') {
65
+ f['prisma/schema.prisma'] = `generator client {
66
+ provider = "prisma-client-js"
67
+ }
68
+
69
+ datasource db {
70
+ provider = "${sqlite ? 'sqlite' : 'postgresql'}"
71
+ url = env("DATABASE_URL")
72
+ }
73
+
74
+ model User {
75
+ id Int @id @default(autoincrement())
76
+ email String @unique
77
+ name String?
78
+ role String @default("user")
79
+ passwordHash String?
80
+ totpSecret String?
81
+ createdAt DateTime @default(now())
82
+ }
83
+
84
+ model Post {
85
+ id Int @id @default(autoincrement())
86
+ title String
87
+ body String?
88
+ published Boolean @default(false)
89
+ createdAt DateTime @default(now())
90
+ }
91
+ `
92
+ f['db/index.ts'] = `import { PrismaClient } from '@prisma/client'
93
+
94
+ const g = globalThis as unknown as { prisma?: PrismaClient }
95
+ export const prisma = (g.prisma ??= new PrismaClient())
96
+ // same names as the other templates, so the rest of the app does not care which ORM you picked
97
+ export const db = { users: prisma.user, posts: prisma.post }
98
+ `
99
+ }
100
+ if (o.orm === 'drizzle') {
101
+ f['db/schema.ts'] = sqlite
102
+ ? `import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core'
103
+
104
+ export const users = sqliteTable('users', {
105
+ id: integer('id').primaryKey({ autoIncrement: true }),
106
+ email: text('email').notNull().unique(),
107
+ name: text('name'),
108
+ role: text('role').notNull().default('user'),
109
+ passwordHash: text('password_hash'),
110
+ totpSecret: text('totp_secret'),
111
+ })
112
+
113
+ export const posts = sqliteTable('posts', {
114
+ id: integer('id').primaryKey({ autoIncrement: true }),
115
+ title: text('title').notNull(),
116
+ body: text('body'),
117
+ published: integer('published', { mode: 'boolean' }).notNull().default(false),
118
+ })
119
+ `
120
+ : `import { pgTable, serial, text, boolean } from 'drizzle-orm/pg-core'
121
+
122
+ export const users = pgTable('users', {
123
+ id: serial('id').primaryKey(),
124
+ email: text('email').notNull().unique(),
125
+ name: text('name'),
126
+ role: text('role').notNull().default('user'),
127
+ passwordHash: text('password_hash'),
128
+ totpSecret: text('totp_secret'),
129
+ })
130
+
131
+ export const posts = pgTable('posts', {
132
+ id: serial('id').primaryKey(),
133
+ title: text('title').notNull(),
134
+ body: text('body'),
135
+ published: boolean('published').notNull().default(false),
136
+ })
137
+ `
138
+ f['db/index.ts'] = sqlite
139
+ ? `import * as schema from './schema'
140
+
141
+ const bun = !!(globalThis as any).Bun
142
+ const url = process.env.DATABASE_URL ?? 'app.db'
143
+ export const db = bun
144
+ ? (await import('drizzle-orm/bun-sqlite')).drizzle(new (await import('bun:sqlite' as string)).Database(url), { schema })
145
+ : (await import('drizzle-orm/better-sqlite3' as string)).drizzle(new (await import('better-sqlite3' as string)).default(url), { schema })
146
+ export { schema }
147
+ `
148
+ : `import postgres from 'postgres'
149
+ import { drizzle } from 'drizzle-orm/postgres-js'
150
+ import * as schema from './schema'
151
+
152
+ export const db = drizzle(postgres(process.env.DATABASE_URL!), { schema })
153
+ export { schema }
154
+ `
155
+ f['drizzle.config.ts'] = `import { defineConfig } from 'drizzle-kit'
156
+
157
+ export default defineConfig({ schema: './db/schema.ts', out: './db/migrations', dialect: '${sqlite ? 'sqlite' : 'postgresql'}', dbCredentials: { url: process.env.DATABASE_URL ?? 'app.db' } })
158
+ `
159
+ }
160
+ return f
161
+ }
162
+
163
+ /** The single config file the user edits later: auth, 2FA, email, i18n, admin roles. */
164
+ export function configFile(o) {
165
+ const lines = [`/**
166
+ * Everything the installer asked lives here. Change it any time.
167
+ * Secrets come from .env (copy .env.example).
168
+ */
169
+ import { defineConfig } from './shared/config'
170
+
171
+ export default defineConfig({
172
+ name: ${JSON.stringify(o.name)},
173
+ url: process.env.APP_URL ?? 'http://localhost:3000',
174
+ `]
175
+ lines.push(` auth: {
176
+ enabled: ${o.auth.length > 0},
177
+ providers: ${JSON.stringify(o.auth)},
178
+ /** TOTP two factor (Google Authenticator, Authy, 1Password) */
179
+ twoFactor: ${o.twofa},
180
+ },
181
+ email: {
182
+ /** resend | postmark | sendgrid | smtp | console */
183
+ provider: ${JSON.stringify(o.email === 'none' ? 'console' : o.email)},
184
+ from: process.env.MAIL_FROM ?? 'hello@example.com',
185
+ },
186
+ i18n: {
187
+ enabled: ${o.i18n},
188
+ locales: ${JSON.stringify(o.i18n ? Object.keys(LOCALES) : ['en'])},
189
+ default: 'en',
190
+ },
191
+ admin: {
192
+ enabled: ${o.admin},
193
+ path: '/admin',
194
+ /**
195
+ * Permissions per role and resource. Actions: list, read, create, update, delete.
196
+ * '*' allows all five. A role missing here cannot open the admin at all.
197
+ */
198
+ roles: ${JSON.stringify(o.roles, null, 2).replace(/\n/g, '\n ')},
199
+ },
200
+ })
201
+ `)
202
+ return lines.join('')
203
+ }
204
+
205
+ export function sharedFiles(o) {
206
+ return {
207
+ 'shared/config.ts': `export type Action = 'list' | 'read' | 'create' | 'update' | 'delete'
208
+ export interface AppConfig {
209
+ name: string
210
+ url: string
211
+ auth: { enabled: boolean; providers: string[]; twoFactor: boolean }
212
+ email: { provider: 'resend' | 'postmark' | 'sendgrid' | 'smtp' | 'console'; from: string }
213
+ i18n: { enabled: boolean; locales: string[]; default: string }
214
+ admin: { enabled: boolean; path: string; roles: Record<string, Record<string, Action[] | '*'>> }
215
+ }
216
+ export const defineConfig = (c: AppConfig) => c
217
+ `,
218
+ 'shared/schemas.ts': `/**
219
+ * Shared between backend and frontend: one schema, validated on the server,
220
+ * typed on the client.
221
+ */
222
+ import { t, type Static } from 'raiko.js'
223
+
224
+ export const NewPost = t.Object({
225
+ title: t.String({ minLength: 1, maxLength: 200 }),
226
+ body: t.Optional(t.String({ maxLength: 10_000 })),
227
+ })
228
+ export type NewPost = Static<typeof NewPost>
229
+
230
+ export const ChatMessage = t.Object({ text: t.String({ minLength: 1, maxLength: 500 }) })
231
+ export type ChatMessage = Static<typeof ChatMessage>
232
+ `,
233
+ }
234
+ }
235
+
236
+ /** Server pieces used by both kinds of templates. Returns code to paste into the server entry. */
237
+ export function serverPieces(o) {
238
+ const imports = [`import { Raiko, isMain } from 'raiko.js'`, `import { cors, secureHeaders, logger, rateLimit, health, openapi } from 'raiko.js/plugins'`, `import config from '${o.serverDepth}raiko.config'`]
239
+ const uses = [` .use(logger())`, ` .use(secureHeaders({ hsts: process.env.NODE_ENV === 'production' }))`, ` .use(cors({ origin: [config.url], credentials: true }))`, ` .use(rateLimit({ max: 300, window: 60_000 }))`, ` .use(health())`]
240
+ if (o.orm !== 'none') imports.push(`import { db } from '${o.serverDepth}db'`)
241
+ if (o.auth.length) {
242
+ imports.push(`import { auth } from '${o.serverDepth}server/auth'`)
243
+ uses.push(` .use(auth.plugin)`)
244
+ }
245
+ if (o.email !== 'none' || o.auth.length) {
246
+ imports.push(`import { mailer } from '${o.serverDepth}emails'`)
247
+ uses.push(` .use(process.env.NODE_ENV !== 'production' ? mailer.preview() : new Raiko())`)
248
+ }
249
+ if (o.i18n) {
250
+ imports.push(`import { i18n } from '${o.serverDepth}locales'`)
251
+ uses.push(` .use(i18n.plugin())`)
252
+ }
253
+ if (o.admin) {
254
+ imports.push(`import { adminPanel } from '${o.serverDepth}server/admin'`)
255
+ uses.push(` .use(adminPanel)`)
256
+ }
257
+ return { imports, uses }
258
+ }
259
+
260
+ export function authFiles(o) {
261
+ if (!o.auth.length) return {}
262
+ const providers = o.auth.filter((p) => p !== 'credentials').map((p) => `${p[0].toUpperCase()}${p.slice(1).replace('github', 'itHub').replace(/^ithub/, 'itHub')}`)
263
+ const names = { github: 'GitHub', google: 'Google', discord: 'Discord', gitlab: 'GitLab' }
264
+ const imports = ['raikoAuth', ...o.auth.map((p) => (p === 'credentials' ? 'Credentials' : names[p])), ...(o.auth.includes('credentials') ? ['verifyPassword'] : [])]
265
+ const list = o.auth.map((p) =>
266
+ p === 'credentials'
267
+ ? ` Credentials({
268
+ async authorize({ email, password }) {
269
+ ${o.orm !== 'none' ? ` const user = await db.users.findUnique({ where: { email: String(email) } })
270
+ if (!user?.passwordHash || !(await verifyPassword(String(password), user.passwordHash))) return null
271
+ return { id: String(user.id), email: user.email, name: user.name, role: user.role }` : ` // no database picked: replace this with your own user lookup
272
+ if (email === 'demo@raiko.dev' && password === 'raiko-demo') return { id: '1', email: 'demo@raiko.dev', role: 'admin' }
273
+ return null`}
274
+ },
275
+ }),`
276
+ : ` ${names[p]}({ clientId: process.env.${p.toUpperCase()}_CLIENT_ID!, clientSecret: process.env.${p.toUpperCase()}_CLIENT_SECRET! }),`,
277
+ )
278
+ void providers
279
+ return {
280
+ 'server/auth.ts': `import { ${imports.join(', ')} } from 'raiko.js/auth'
281
+ ${o.orm !== 'none' ? "import { db } from '../db'\n" : ''}import config from '../raiko.config'
282
+
283
+ export const auth = raikoAuth({
284
+ // a missing secret in production would let anyone forge sessions, so it refuses to start
285
+ // the fallback exists only while developing; anywhere else a missing secret refuses to start,
286
+ // because a known secret would let anyone forge an admin session
287
+ // the installer wrote a random AUTH_SECRET to .env; a missing one refuses to start,
288
+ // because a known secret would let anyone forge an admin session
289
+ secret:
290
+ process.env.AUTH_SECRET ||
291
+ (() => {
292
+ throw new Error('AUTH_SECRET is not set. Generate one with: npx raiko secret')
293
+ })(),
294
+ url: config.url,
295
+ pages: { signIn: '/login', twoFactor: '/login/2fa' },
296
+ providers: [
297
+ ${list.join('\n')}
298
+ ],${o.twofa ? `
299
+ twoFactor: config.auth.twoFactor
300
+ ? {
301
+ // return the user's TOTP secret when they turned 2FA on
302
+ getSecret: async (user) => ${o.orm !== 'none' ? `(await db.users.findUnique({ where: { id: Number(user.id) } }))?.totpSecret ?? null` : 'null'},
303
+ }
304
+ : undefined,` : ''}
305
+ })
306
+ `,
307
+ }
308
+ }
309
+
310
+ export function emailFiles(o) {
311
+ if (o.email === 'none' && !o.auth.length) return {}
312
+ const transport = { resend: `resend({ apiKey: process.env.RESEND_API_KEY! })`, postmark: `postmark({ serverToken: process.env.POSTMARK_TOKEN! })`, sendgrid: `sendgrid({ apiKey: process.env.SENDGRID_API_KEY! })`, smtp: `smtp({ host: process.env.SMTP_HOST!, port: Number(process.env.SMTP_PORT ?? 587), user: process.env.SMTP_USER, pass: process.env.SMTP_PASS })`, none: `consoleTransport()` }[o.email]
313
+ const fn = { resend: 'resend', postmark: 'postmark', sendgrid: 'sendgrid', smtp: 'smtp', none: 'consoleTransport' }[o.email]
314
+ return {
315
+ 'emails/welcome.tsx': `/** @jsxImportSource raiko.js */
316
+ /** Preview it at /_raiko/mail/welcome while running the dev server. */
317
+ export default function Welcome({ name }: { name: string }) {
318
+ return (
319
+ <div style={{ fontFamily: 'system-ui, sans-serif', maxWidth: '520px', margin: '0 auto', padding: '32px' }}>
320
+ <h1 style={{ fontSize: '22px' }}>Welcome, {name}</h1>
321
+ <p>Your account is ready.</p>
322
+ <a href="https://example.com" style={{ background: '#ffd23f', color: '#0a0a0b', padding: '10px 18px', borderRadius: '6px', textDecoration: 'none', fontWeight: 600 }}>Open the app</a>
323
+ </div>
324
+ )
325
+ }
326
+ Welcome.subject = (p: { name: string }) => \`Welcome, \${p.name}\`
327
+ Welcome.preview = { name: 'Ana' }
328
+ `,
329
+ 'emails/index.ts': `import { createMailer, ${fn}${fn !== 'consoleTransport' ? ', consoleTransport' : ''} } from 'raiko.js/mail'
330
+ import config from '../raiko.config'
331
+ import Welcome from './welcome'
332
+
333
+ export const mailer = createMailer({
334
+ from: config.email.from,
335
+ // print emails in development, send them in production
336
+ transport: process.env.NODE_ENV === 'production' ? ${transport} : consoleTransport(),
337
+ templates: { welcome: Welcome },
338
+ })
339
+ `,
340
+ }
341
+ }
342
+
343
+ export function i18nFiles(o) {
344
+ if (!o.i18n) return {}
345
+ const f = {}
346
+ for (const l of Object.keys(LOCALES)) f[`locales/${l}.ts`] = `export default ${JSON.stringify(T[l], null, 2)} as const\n`
347
+ f['locales/index.ts'] = `import { createI18n } from 'raiko.js/i18n'
348
+ ${Object.keys(LOCALES).map((l) => `import ${l.replace('-', '')} from './${l}'`).join('\n')}
349
+
350
+ export const i18n = createI18n({ ${Object.keys(LOCALES).map((l) => (l.includes('-') ? `'${l}': ${l.replace('-', '')}` : l)).join(', ')} }, { fallback: 'en', cookie: 'locale' })
351
+ `
352
+ return f
353
+ }
354
+
355
+ export function adminFiles(o) {
356
+ if (!o.admin) return {}
357
+ const adapter = o.orm === 'drizzle' ? `drizzleAdapter(db, schema.users, { id: schema.users.id, searchable: [schema.users.email] })` : `modelAdapter(db.users, { searchable: ['email', 'name'] })`
358
+ const adapter2 = o.orm === 'drizzle' ? `drizzleAdapter(db, schema.posts, { id: schema.posts.id, searchable: [schema.posts.title] })` : `modelAdapter(db.posts, { searchable: ['title'] })`
359
+ const perm = (res) => `Object.fromEntries(Object.entries(config.admin.roles).map(([role, r]) => [role, r.${res} ?? []]))`
360
+ return {
361
+ 'server/admin.ts': `import { admin, ${o.orm === 'drizzle' ? 'drizzleAdapter' : 'modelAdapter'} } from 'raiko.js/admin'
362
+ import { db${o.orm === 'drizzle' ? ', schema' : ''} } from '../db'
363
+ import config from '../raiko.config'
364
+
365
+ /** Permissions come from raiko.config.ts, edit them there. */
366
+ export const adminPanel = admin({
367
+ path: config.admin.path,
368
+ title: \`\${config.name} admin\`,
369
+ ${o.auth.length ? `auth: ({ auth }: any) => auth?.user?.role ?? null,` : `// no auth picked: everyone is admin in development, nobody in production
370
+ auth: () => (process.env.NODE_ENV === 'production' ? null : 'admin'),`}
371
+ signIn: '/login',
372
+ resources: {
373
+ users: { adapter: ${adapter}, fields: ['id', 'email', 'name', 'role'], hidden: ['passwordHash', 'totpSecret'], permissions: ${perm('users')} },
374
+ posts: { adapter: ${adapter2}, fields: ['id', 'title', 'published'], permissions: ${perm('posts')} },
375
+ },
376
+ })
377
+ `,
378
+ }
379
+ }
380
+
381
+ export function deployFiles(o) {
382
+ const f = {}
383
+ const out = isVite(o) ? 'dist' : 'public'
384
+ if (o.deploy === 'vercel') {
385
+ f['api/index.ts'] = `import app from '../${isVite(o) ? 'server/index' : 'server'}'\n\nexport default { fetch: app.fetch }\n`
386
+ f['vercel.json'] = json({
387
+ $schema: 'https://openapi.vercel.sh/vercel.json',
388
+ buildCommand: isVite(o) ? 'npx raiko manifest && npx vite build' : 'npx raiko manifest',
389
+ outputDirectory: out,
390
+ rewrites: isVite(o) ? [{ source: '/(api|auth|admin|docs|_raiko|health)(.*)', destination: '/api' }, { source: '/(.*)', destination: '/index.html' }] : [{ source: '/(.*)', destination: '/api' }],
391
+ })
392
+ }
393
+ if (o.deploy === 'cloudflare') {
394
+ f['worker.ts'] = `import app from './${isVite(o) ? 'server/index' : 'server'}'\n\nexport default { fetch: app.fetch }\n`
395
+ f['wrangler.toml'] = `name = ${JSON.stringify(o.name)}\nmain = "worker.ts"\n# 2025-04-01 or later fills process.env from vars and secrets: npx wrangler secret put AUTH_SECRET\ncompatibility_date = "2025-04-01"\ncompatibility_flags = ["nodejs_compat"]\n\n[vars]\nNODE_ENV = "production"\n\n[assets]\ndirectory = "./${out}"\n${isVite(o) ? 'not_found_handling = "single-page-application"\n' : ''}`
396
+ }
397
+ if (o.deploy === 'docker' || o.deploy === 'node') {
398
+ const assets = isVite(o) ? 'dist' : 'public'
399
+ f['Dockerfile'] = `# small production image, Bun runtime, runs as a non root user
400
+ FROM oven/bun:1.1-alpine AS build
401
+ WORKDIR /app
402
+ COPY package.json ./
403
+ RUN bun install
404
+ COPY . .
405
+ RUN bun run build && rm -rf node_modules && bun install --production
406
+
407
+ FROM oven/bun:1.1-alpine
408
+ WORKDIR /app
409
+ ENV NODE_ENV=production
410
+ # only what runs: no source, no .env files, no local databases
411
+ COPY --from=build --chown=bun:bun /app/package.json ./
412
+ COPY --from=build --chown=bun:bun /app/node_modules ./node_modules
413
+ COPY --from=build --chown=bun:bun /app/.raiko/out ./.raiko/out
414
+ COPY --from=build --chown=bun:bun /app/${assets} ./${assets}
415
+ USER bun
416
+ EXPOSE 3000
417
+ CMD ["bun", "run", "start"]
418
+ `
419
+ f['.dockerignore'] = 'node_modules\n.git\n.env\n.env.*\n!.env.example\n*.db\n*.db-journal\n*.sqlite\n.raiko\ndist\n*.log\n.vercel\n.wrangler\n'
420
+ }
421
+ return f
422
+ }
423
+
424
+ export function envExample(o) {
425
+ const l = ['APP_URL=http://localhost:3000', 'PORT=3000']
426
+ if (o.orm !== 'none') l.push(o.db === 'sqlite' ? (o.orm === 'prisma' ? 'DATABASE_URL=file:./app.db' : 'DATABASE_URL=app.db') : 'DATABASE_URL=postgres://user:pass@localhost:5432/app')
427
+ if (o.auth.length) l.push('# generate with: npx raiko secret', 'AUTH_SECRET=')
428
+ for (const p of o.auth.filter((p) => p !== 'credentials')) l.push(`${p.toUpperCase()}_CLIENT_ID=`, `${p.toUpperCase()}_CLIENT_SECRET=`)
429
+ if (o.email !== 'none') l.push('MAIL_FROM=hello@example.com')
430
+ if (o.email === 'resend') l.push('RESEND_API_KEY=')
431
+ if (o.email === 'postmark') l.push('POSTMARK_TOKEN=')
432
+ if (o.email === 'sendgrid') l.push('SENDGRID_API_KEY=')
433
+ if (o.email === 'smtp') l.push('SMTP_HOST=', 'SMTP_PORT=587', 'SMTP_USER=', 'SMTP_PASS=')
434
+ return l.join('\n') + '\n'
435
+ }
436
+
437
+ export function gitignore() {
438
+ return 'node_modules\ndist\n.raiko\n.env\n.env.*\n!.env.example\n*.log\n*.db\n.vercel\n.wrangler\n.DS_Store\n'
439
+ }