@murumets-ee/create 0.1.7 → 0.1.9

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/dist/index.cjs ADDED
@@ -0,0 +1,2103 @@
1
+ "use strict";var A=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var H=(e,a)=>()=>(e&&(a=e(e=0)),a);var P=(e,a)=>{for(var r in a)A(e,r,{get:a[r],enumerable:!0})},W=(e,a,r,o)=>{if(a&&typeof a=="object"||typeof a=="function")for(let c of D(a))!B.call(e,c)&&c!==r&&A(e,c,{get:()=>a[c],enumerable:!(o=F(a,c))||o.enumerable});return e};var G=e=>W(A({},"__esModule",{value:!0}),e);var T={};P(T,{appendToFile:()=>N,deleteFiles:()=>g,mergePackageJson:()=>d,runCommand:()=>u,writeFile:()=>t});async function t(e,a){await(0,m.mkdir)((0,b.dirname)(e),{recursive:!0}),await(0,m.writeFile)(e,a,"utf-8")}async function d(e,a){let r=await(0,m.readFile)(e,"utf-8"),o=JSON.parse(r);a.type&&(o.type=a.type),a.dependencies&&(o.dependencies={...o.dependencies,...a.dependencies}),a.devDependencies&&(o.devDependencies={...o.devDependencies,...a.devDependencies}),a.scripts&&(o.scripts={...o.scripts,...a.scripts}),await(0,m.writeFile)(e,`${JSON.stringify(o,null,2)}
2
+ `,"utf-8")}async function g(e,a){for(let r of a)await(0,m.rm)((0,b.join)(e,r),{force:!0,recursive:!0})}function u(e,a){return(0,R.execSync)(e,{cwd:a?.cwd,stdio:"pipe",encoding:"utf-8"})}async function N(e,a){await(0,m.appendFile)(e,a,"utf-8")}var R,m,b,l=H(()=>{"use strict";R=require("child_process"),m=require("fs/promises"),b=require("path")});var X={};P(X,{scaffold:()=>U});module.exports=G(X);var k=require("path");var s=require("path");l();async function z(e,a){await t((0,s.join)(e,"lib/auth.ts"),`import { getToolkitApp } from './app'
3
+ import { getAuth } from '@murumets-ee/auth'
4
+
5
+ export async function getAuthInstance() {
6
+ await getToolkitApp()
7
+ return getAuth()
8
+ }
9
+ `),await t((0,s.join)(e,"lib/auth-client.ts"),`import { createClient } from '@murumets-ee/auth/client'
10
+
11
+ export const authClient = createClient()
12
+ `),await t((0,s.join)(e,"app/api/auth/[...all]/route.ts"),`import { toNextJsHandler } from 'better-auth/next-js'
13
+ import { getAuthInstance } from '../../../../lib/auth'
14
+
15
+ let _handler: ReturnType<typeof toNextJsHandler> | null = null
16
+
17
+ async function handler() {
18
+ if (!_handler) {
19
+ const auth = await getAuthInstance()
20
+ _handler = toNextJsHandler(auth)
21
+ }
22
+ return _handler
23
+ }
24
+
25
+ export async function GET(req: Request) {
26
+ const h = await handler()
27
+ return h.GET(req)
28
+ }
29
+
30
+ export async function POST(req: Request) {
31
+ const h = await handler()
32
+ return h.POST(req)
33
+ }
34
+ `),await t((0,s.join)(e,"app/[locale]/auth/layout.tsx"),`import { AuthProviders } from './providers'
35
+ import type { ReactNode } from 'react'
36
+
37
+ export default function AuthLayout({ children }: { children: ReactNode }) {
38
+ return (
39
+ <AuthProviders>
40
+ <div className="flex min-h-[calc(100vh-3rem)] items-center justify-center px-4">
41
+ {children}
42
+ </div>
43
+ </AuthProviders>
44
+ )
45
+ }
46
+ `),await t((0,s.join)(e,"app/[locale]/auth/providers.tsx"),`'use client'
47
+
48
+ import { AuthUIProvider } from '@murumets-ee/auth-ui'
49
+ import { authClient } from '@/lib/auth-client'
50
+ import Link from 'next/link'
51
+ import { useRouter } from 'next/navigation'
52
+ import { useLocale } from 'next-intl'
53
+ import type { ReactNode } from 'react'
54
+
55
+ export function AuthProviders({ children }: { children: ReactNode }) {
56
+ const router = useRouter()
57
+ const locale = useLocale()
58
+
59
+ return (
60
+ <AuthUIProvider
61
+ authClient={authClient as never}
62
+ basePath="/auth"
63
+ redirectTo="/"
64
+ Link={Link}
65
+ navigate={(url) => router.push(url)}
66
+ resetPasswordUrl={\`/\${locale}/auth/reset-password\`}
67
+ >
68
+ {children}
69
+ </AuthUIProvider>
70
+ )
71
+ }
72
+ `),await t((0,s.join)(e,"app/[locale]/auth/sign-in/page.tsx"),`import { SignInForm } from '@murumets-ee/auth-ui'
73
+
74
+ export default function SignInPage() {
75
+ return <SignInForm />
76
+ }
77
+ `),await t((0,s.join)(e,"app/[locale]/auth/sign-up/page.tsx"),`import { SignUpForm } from '@murumets-ee/auth-ui'
78
+
79
+ export default function SignUpPage() {
80
+ return <SignUpForm />
81
+ }
82
+ `),await t((0,s.join)(e,"app/[locale]/auth/forgot-password/page.tsx"),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
83
+
84
+ export default function ForgotPasswordPage() {
85
+ return <ForgotPasswordForm />
86
+ }
87
+ `),await t((0,s.join)(e,"app/[locale]/auth/reset-password/page.tsx"),`import { Suspense } from 'react'
88
+ import { ResetPasswordContent } from './content'
89
+
90
+ export default function ResetPasswordPage() {
91
+ return (
92
+ <Suspense>
93
+ <ResetPasswordContent />
94
+ </Suspense>
95
+ )
96
+ }
97
+ `),await t((0,s.join)(e,"app/[locale]/auth/reset-password/content.tsx"),`'use client'
98
+
99
+ import { useSearchParams } from 'next/navigation'
100
+ import { ResetPasswordForm } from '@murumets-ee/auth-ui'
101
+
102
+ export function ResetPasswordContent() {
103
+ const searchParams = useSearchParams()
104
+ const token = searchParams.get('token')
105
+
106
+ if (!token) {
107
+ return (
108
+ <div className="text-center">
109
+ <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
110
+ Invalid or expired link
111
+ </h1>
112
+ <p className="mt-2 text-zinc-500">
113
+ Please request a new password reset.
114
+ </p>
115
+ </div>
116
+ )
117
+ }
118
+
119
+ return <ResetPasswordForm token={token} />
120
+ }
121
+ `),await t((0,s.join)(e,"app/[locale]/setup/page.tsx"),`import { redirect } from 'next/navigation'
122
+ import { sql } from 'drizzle-orm'
123
+ import { getToolkitApp } from '@/lib/app'
124
+ import { SetupForm } from './form'
125
+
126
+ export default async function SetupPage() {
127
+ const app = await getToolkitApp()
128
+ const result = await app.db.readOnly.execute<{ count: string }>(
129
+ sql\`SELECT COUNT(*)::text as count FROM "user"\`,
130
+ )
131
+
132
+ if (Number(result[0]?.count) > 0) {
133
+ redirect('/')
134
+ }
135
+
136
+ return (
137
+ <div className="max-w-md mx-auto p-8">
138
+ <h1 className="text-2xl font-bold mb-2">Create Admin</h1>
139
+ <p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
140
+ No users found. Create the first admin account.
141
+ </p>
142
+ <SetupForm />
143
+ </div>
144
+ )
145
+ }
146
+ `),await t((0,s.join)(e,"app/[locale]/setup/form.tsx"),`'use client'
147
+
148
+ import { useState } from 'react'
149
+ import { createFirstAdmin } from './actions'
150
+
151
+ export function SetupForm() {
152
+ const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)
153
+ const [loading, setLoading] = useState(false)
154
+
155
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
156
+ e.preventDefault()
157
+ setLoading(true)
158
+ setResult(null)
159
+ const res = await createFirstAdmin(new FormData(e.currentTarget))
160
+ setResult(res)
161
+ setLoading(false)
162
+ }
163
+
164
+ if (result?.ok) {
165
+ return (
166
+ <div className="space-y-2">
167
+ <p className="text-green-600 dark:text-green-400">Admin created: {result.email}</p>
168
+ <a href="/auth/sign-in" className="text-blue-600 dark:text-blue-400 hover:underline">
169
+ Sign in &rarr;
170
+ </a>
171
+ </div>
172
+ )
173
+ }
174
+
175
+ return (
176
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
177
+ <label className="text-sm font-medium">
178
+ Name
179
+ <input
180
+ name="name"
181
+ required
182
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
183
+ />
184
+ </label>
185
+ <label className="text-sm font-medium">
186
+ Email
187
+ <input
188
+ name="email"
189
+ type="email"
190
+ required
191
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
192
+ />
193
+ </label>
194
+ <label className="text-sm font-medium">
195
+ Password (min 8 chars)
196
+ <input
197
+ name="password"
198
+ type="password"
199
+ required
200
+ minLength={8}
201
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
202
+ />
203
+ </label>
204
+
205
+ {result?.error && <p className="text-red-600 dark:text-red-400 text-sm">{result.error}</p>}
206
+
207
+ <button
208
+ type="submit"
209
+ disabled={loading}
210
+ className="px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors"
211
+ >
212
+ {loading ? 'Creating...' : 'Create Admin'}
213
+ </button>
214
+ </form>
215
+ )
216
+ }
217
+ `),await t((0,s.join)(e,"app/[locale]/setup/actions.ts"),`'use server'
218
+
219
+ import { sql } from 'drizzle-orm'
220
+ import { getToolkitApp } from '@/lib/app'
221
+ import { getAuth } from '@murumets-ee/auth'
222
+
223
+ /**
224
+ * Create the first admin user. Locked down:
225
+ * - Advisory lock prevents race conditions (two simultaneous requests)
226
+ * - User count check inside the lock ensures only one admin can be created
227
+ * - Once any user exists, this action permanently refuses
228
+ */
229
+ export async function createFirstAdmin(formData: FormData) {
230
+ const email = formData.get('email') as string
231
+ const password = formData.get('password') as string
232
+ const name = formData.get('name') as string
233
+
234
+ if (!email || !password || !name) {
235
+ return { error: 'All fields are required' }
236
+ }
237
+
238
+ if (password.length < 8) {
239
+ return { error: 'Password must be at least 8 characters' }
240
+ }
241
+
242
+ const app = await getToolkitApp()
243
+
244
+ // Acquire advisory lock + check atomically in a transaction
245
+ const canCreate = await app.db.readWrite.transaction(async (tx) => {
246
+ // Advisory lock 1 = "setup lock". Blocks concurrent setup attempts.
247
+ await tx.execute(sql\`SELECT pg_advisory_xact_lock(1)\`)
248
+
249
+ const result = await tx.execute<{ count: string }>(
250
+ sql\`SELECT COUNT(*)::text as count FROM "user"\`,
251
+ )
252
+ return Number(result[0]?.count) === 0
253
+ })
254
+
255
+ if (!canCreate) {
256
+ return { error: 'Admin user already exists. Setup is complete.' }
257
+ }
258
+
259
+ const auth = getAuth()
260
+
261
+ const adminUser = await auth.api.signUpEmail({
262
+ body: { email, password, name },
263
+ })
264
+
265
+ await app.db.readWrite.execute(
266
+ sql\`UPDATE "user" SET role = 'admin' WHERE id = \${adminUser.user.id}\`,
267
+ )
268
+
269
+ return { ok: true, email: adminUser.user.email }
270
+ }
271
+ `)}var p=require("path");l();async function C(e,a){let{name:r}=a;await t((0,p.join)(e,".env.example"),`DATABASE_URL=postgresql://${r}:${r}_dev_password@localhost:5432/${r}_dev
272
+ BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
273
+ BETTER_AUTH_URL=http://localhost:3000
274
+ LOG_LEVEL=debug
275
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
276
+ QUEUE_WORKER=true
277
+ RESEND_API_KEY=
278
+ RESEND_WEBHOOK_SECRET=
279
+ MAIL_FROM=noreply@example.com
280
+ CSAT_SECRET=
281
+ `),await t((0,p.join)(e,".dockerignore"),`# Dependencies (installed inside Docker)
282
+ node_modules/
283
+ .pnpm-store/
284
+
285
+ # Build outputs (rebuilt inside Docker)
286
+ .next/
287
+ out/
288
+ dist/
289
+
290
+ # Git
291
+ .git/
292
+ .gitignore
293
+
294
+ # Environment files (pass via docker run --env-file)
295
+ .env
296
+ .env.*
297
+ !.env.example
298
+
299
+ # Docker files
300
+ Dockerfile*
301
+ .dockerignore
302
+ docker-compose*.yml
303
+
304
+ # Tests
305
+ coverage/
306
+ **/*.test.*
307
+ **/*.spec.*
308
+ **/vitest.config.*
309
+ docker-compose.test.yml
310
+
311
+ # Development tooling
312
+ .vscode/
313
+ .idea/
314
+ .turbo/
315
+ .cache/
316
+ *.tsbuildinfo
317
+
318
+ # Documentation
319
+ *.md
320
+ docs/
321
+
322
+ # OS files
323
+ .DS_Store
324
+ Thumbs.db
325
+ `),await t((0,p.join)(e,"Dockerfile"),`# --- Base image ---
326
+ # Node 22 LTS (Debian slim \u2014 glibc required for sharp image optimization)
327
+ FROM node:22-slim AS base
328
+
329
+ # --- Stage 1: Install dependencies ---
330
+ FROM base AS deps
331
+ WORKDIR /app
332
+ RUN corepack enable pnpm
333
+ COPY package.json pnpm-lock.yaml ./
334
+ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \\
335
+ pnpm install --frozen-lockfile
336
+
337
+ # --- Stage 2: Build the application ---
338
+ FROM base AS builder
339
+ WORKDIR /app
340
+ RUN corepack enable pnpm
341
+ COPY --from=deps /app/node_modules ./node_modules
342
+ COPY . .
343
+ ENV NODE_ENV=production
344
+ ENV NEXT_TELEMETRY_DISABLED=1
345
+ # Dummy env vars so toolkit.config.ts doesn't throw at build time (no DB connection needed)
346
+ ENV DATABASE_URL=postgresql://build:build@localhost:5432/build
347
+ ENV BETTER_AUTH_SECRET=build-secret-not-used-at-runtime
348
+ RUN pnpm build
349
+
350
+ # --- Stage 3: Production runner ---
351
+ FROM base AS runner
352
+ WORKDIR /app
353
+ ENV NODE_ENV=production
354
+ ENV PORT=3000
355
+ ENV HOSTNAME=0.0.0.0
356
+ ENV NEXT_TELEMETRY_DISABLED=1
357
+
358
+ # Copy standalone output (includes server.js + traced node_modules)
359
+ COPY --from=builder --chown=node:node /app/.next/standalone ./
360
+ # Copy static assets (JS/CSS chunks \u2014 excluded from standalone trace)
361
+ COPY --from=builder --chown=node:node /app/.next/static ./.next/static
362
+ # Copy public assets (favicon, robots.txt, images)
363
+ COPY --from=builder --chown=node:node /app/public ./public
364
+ # Copy migrations (applied at app startup by the toolkit migration runner)
365
+ COPY --from=builder --chown=node:node /app/migrations ./migrations
366
+
367
+ # Run as non-root user
368
+ USER node
369
+ EXPOSE 3000
370
+ CMD ["node", "server.js"]
371
+ `),await t((0,p.join)(e,"docker-compose.yml"),`services:
372
+ postgres:
373
+ image: postgres:17-alpine
374
+ container_name: ${r}-postgres
375
+ restart: unless-stopped
376
+ environment:
377
+ POSTGRES_USER: ${r}
378
+ POSTGRES_PASSWORD: ${r}_dev_password
379
+ POSTGRES_DB: ${r}_dev
380
+ ports:
381
+ - "5432:5432"
382
+ volumes:
383
+ - postgres_data:/var/lib/postgresql/data
384
+ healthcheck:
385
+ test: ["CMD-SHELL", "pg_isready -U ${r} -d ${r}_dev"]
386
+ interval: 10s
387
+ timeout: 5s
388
+ retries: 5
389
+
390
+ volumes:
391
+ postgres_data:
392
+ name: ${r}_postgres_data
393
+ `),await t((0,p.join)(e,"docker-compose.prod.yml"),`services:
394
+ app:
395
+ build: .
396
+ restart: unless-stopped
397
+ env_file: .env
398
+ depends_on:
399
+ postgres:
400
+ condition: service_healthy
401
+ ports:
402
+ - "3000:3000"
403
+ networks:
404
+ - internal
405
+
406
+ postgres:
407
+ image: postgres:17-alpine
408
+ restart: unless-stopped
409
+ environment:
410
+ POSTGRES_USER: ${r}
411
+ POSTGRES_PASSWORD: \${DB_PASSWORD}
412
+ POSTGRES_DB: ${r}
413
+ volumes:
414
+ - postgres_data:/var/lib/postgresql/data
415
+ healthcheck:
416
+ test: ["CMD-SHELL", "pg_isready -U ${r} -d ${r}"]
417
+ interval: 10s
418
+ timeout: 5s
419
+ retries: 5
420
+ networks:
421
+ - internal
422
+
423
+ volumes:
424
+ postgres_data:
425
+
426
+ networks:
427
+ internal:
428
+ `),await N((0,p.join)(e,".gitignore"),`
429
+ # Toolkit generated files
430
+ generated/
431
+
432
+ # Environment
433
+ .env*
434
+ !.env.example
435
+ `)}var v=require("path");l();async function L(e,a){await t((0,v.join)(e,"entities/index.ts"),`export { Article } from './article'
436
+ export { Category } from './category'
437
+ `),await t((0,v.join)(e,"entities/article.ts"),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
438
+
439
+ export const Article = defineEntity({
440
+ name: 'article',
441
+ fields: {
442
+ title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),
443
+ slug: field.slug({ from: 'title', unique: true }),
444
+ excerpt: field.text({ maxLength: 500, translatable: true }),
445
+ body: field.richtext(),
446
+ viewCount: field.number({ default: 0, integer: true }),
447
+ featured: field.boolean({ default: false }),
448
+ publishDate: field.date(),
449
+ contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),
450
+ category: field.reference({ entity: 'category', required: false }),
451
+ tags: field.reference({ entity: 'category', cardinality: 'many' }),
452
+ coverImage: field.media({ accept: ['image/*'] }),
453
+ },
454
+ behaviors: [
455
+ behavior.publishable(),
456
+ behavior.auditable(),
457
+ behavior.sluggable('title'),
458
+ behavior.revisionable(),
459
+ ],
460
+ scope: 'global',
461
+ access: {
462
+ view: 'public',
463
+ create: 'group.editor',
464
+ update: 'group.editor',
465
+ delete: 'group.admin',
466
+ },
467
+ })
468
+ `),await t((0,v.join)(e,"entities/category.ts"),`import { defineEntity, field } from '@murumets-ee/entity'
469
+
470
+ export const Category = defineEntity({
471
+ name: 'category',
472
+ fields: {
473
+ name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),
474
+ slug: field.slug({ from: 'name', unique: true }),
475
+ description: field.text({ maxLength: 500, translatable: true }),
476
+ },
477
+ scope: 'global',
478
+ access: {
479
+ view: 'public',
480
+ create: 'group.editor',
481
+ update: 'group.editor',
482
+ delete: 'group.admin',
483
+ },
484
+ })
485
+ `)}var S=require("path");l();async function _(e,a){await t((0,S.join)(e,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
486
+
487
+ export const routing = defineRouting({
488
+ locales: ['en'],
489
+ defaultLocale: 'en',
490
+ })
491
+ `),await t((0,S.join)(e,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
492
+ import { hasLocale } from 'next-intl'
493
+ import { routing } from './routing'
494
+ import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
495
+
496
+ export default getRequestConfig(async ({ requestLocale }) => {
497
+ const requested = await requestLocale
498
+ const locale = hasLocale(routing.locales, requested)
499
+ ? requested
500
+ : routing.defaultLocale
501
+
502
+ const authMessages = await getAuthMessages(locale)
503
+
504
+ return {
505
+ locale,
506
+ messages: {
507
+ ...authMessages,
508
+ },
509
+ }
510
+ })
511
+ `)}var E=require("fs/promises"),i=require("path");l();var n={myorgCore:"0.1.0",myorgDb:"0.1.0",myorgEntity:"0.1.0",myorgLogging:"0.1.0",myorgAuth:"0.1.0",myorgAuthUi:"0.1.0",myorgAdminUi:"0.1.0",myorgContent:"0.1.0",myorgContentApi:"0.1.0",myorgSettings:"0.1.0",myorgStorage:"0.1.0",myorgMedia:"0.1.0",myorgTaxonomy:"0.1.0",myorgQueue:"0.1.0",myorgMail:"0.1.0",myorgTicketing:"0.1.0",myorgTicketingUi:"0.1.0",myorgEditor:"0.1.0",myorgBlocks:"0.1.0",myorgTokens:"0.1.0",myorgUi:"0.1.0",myorgCli:"0.1.0",betterAuth:"1.4.0",drizzleOrm:"0.45.1",nextIntl:"4.8.2",nextThemes:"0.4.6",lucideReact:"0.563.0",postgres:"3.4.5",reactHookForm:"7.71.1",hookformResolvers:"5.2.2",zod:"3.24.1",drizzleKit:"0.27.2",tsx:"4.19.2",babelReactCompiler:"1.0.0"};async function $(e,a,r){await j(e,a),r?.("Workspace root created"),r?.("Creating admin app..."),await V(e,a),r?.("Admin app created"),r?.("Creating web app..."),await J(e,a),r?.("Web app created"),await K(e,a),r?.("Shared config package created")}async function j(e,a){let{name:r}=a;await(0,E.mkdir)(e,{recursive:!0}),await t((0,i.join)(e,"pnpm-workspace.yaml"),`packages:
512
+ - 'packages/*'
513
+ - 'apps/*'
514
+
515
+ ignoredBuiltDependencies:
516
+ - sharp
517
+ - unrs-resolver
518
+ `),await t((0,i.join)(e,"package.json"),`${JSON.stringify({name:`@${r}/monorepo`,version:"0.0.0",private:!0,type:"module",scripts:{dev:"turbo dev",build:"turbo build","db:generate":"tsx --env-file=.env packages/config/scripts/generate-schema.ts","db:migrate:generate":"drizzle-kit generate --config packages/config/drizzle.config.ts","db:migrate":"tsx --env-file=.env packages/config/scripts/migrate.ts","db:reset":"tsx --env-file=.env packages/config/scripts/reset-db.ts"},devDependencies:{turbo:"^2.3.3",typescript:"^5.7.3",tsx:`^${n.tsx}`,"drizzle-kit":`^${n.drizzleKit}`}},null,2)}
519
+ `),await t((0,i.join)(e,".gitignore"),`# dependencies
520
+ node_modules/
521
+
522
+ # build
523
+ dist/
524
+ .next/
525
+ .turbo/
526
+
527
+ # env
528
+ .env*
529
+ !.env.example
530
+
531
+ # toolkit generated
532
+ generated/
533
+
534
+ # IDE
535
+ .vscode/
536
+ .idea/
537
+
538
+ # OS
539
+ .DS_Store
540
+ Thumbs.db
541
+
542
+ # logs
543
+ *.log
544
+
545
+ # migrations meta
546
+ migrations/
547
+
548
+ # turbo
549
+ .turbo/
550
+ `),await t((0,i.join)(e,".env.example"),`DATABASE_URL=postgresql://${r}:${r}_dev_password@localhost:5432/${r}_dev
551
+ BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
552
+ BETTER_AUTH_URL=http://localhost:3000
553
+ LOG_LEVEL=debug
554
+ NEXT_PUBLIC_APP_URL=http://localhost:3000
555
+ QUEUE_WORKER=true
556
+ RESEND_API_KEY=
557
+ RESEND_WEBHOOK_SECRET=
558
+ MAIL_FROM=noreply@example.com
559
+ CSAT_SECRET=
560
+ `),await t((0,i.join)(e,"docker-compose.yml"),`services:
561
+ postgres:
562
+ image: postgres:17-alpine
563
+ container_name: ${r}-postgres
564
+ restart: unless-stopped
565
+ environment:
566
+ POSTGRES_USER: ${r}
567
+ POSTGRES_PASSWORD: ${r}_dev_password
568
+ POSTGRES_DB: ${r}_dev
569
+ ports:
570
+ - "5432:5432"
571
+ volumes:
572
+ - postgres_data:/var/lib/postgresql/data
573
+ healthcheck:
574
+ test: ["CMD-SHELL", "pg_isready -U ${r} -d ${r}_dev"]
575
+ interval: 10s
576
+ timeout: 5s
577
+ retries: 5
578
+
579
+ volumes:
580
+ postgres_data:
581
+ name: ${r}_postgres_data
582
+ `)}async function K(e,a){let{name:r}=a,o=(0,i.join)(e,"packages/config");await t((0,i.join)(o,"package.json"),`${JSON.stringify({name:`@${r}/config`,version:"0.1.0",private:!0,type:"module",exports:{".":"./toolkit.config.ts","./entities":"./entities/index.ts","./entities/*":"./entities/*","./app":"./lib/app.ts","./auth":"./lib/auth.ts","./auth-client":"./lib/auth-client.ts"},dependencies:{"@murumets-ee/core":`^${n.myorgCore}`,"@murumets-ee/db":`^${n.myorgDb}`,"@murumets-ee/entity":`^${n.myorgEntity}`,"@murumets-ee/logging":`^${n.myorgLogging}`,"@murumets-ee/auth":`^${n.myorgAuth}`,"better-auth":`^${n.betterAuth}`,"drizzle-orm":`^${n.drizzleOrm}`,postgres:`^${n.postgres}`,zod:`^${n.zod}`}},null,2)}
583
+ `),await t((0,i.join)(o,"tsconfig.json"),`${JSON.stringify({compilerOptions:{target:"ES2022",module:"ESNext",moduleResolution:"Bundler",strict:!0,esModuleInterop:!0,skipLibCheck:!0,resolveJsonModule:!0},include:["**/*.ts"]},null,2)}
584
+ `),await t((0,i.join)(o,"toolkit.config.ts"),`import { auth } from '@murumets-ee/auth/plugin'
585
+ import { content } from '@murumets-ee/content/plugin'
586
+ import { defineConfig } from '@murumets-ee/core'
587
+ import { logging } from '@murumets-ee/logging/plugin'
588
+ import { mail, ResendMailProvider } from '@murumets-ee/mail'
589
+ import { media } from '@murumets-ee/media/plugin'
590
+ import { queue } from '@murumets-ee/queue/plugin'
591
+ import { settings } from '@murumets-ee/settings/plugin'
592
+ import { storage } from '@murumets-ee/storage/plugin'
593
+ import { taxonomy } from '@murumets-ee/taxonomy/plugin'
594
+ import { ticketing } from '@murumets-ee/ticketing/plugin'
595
+ import { Article, Category } from './entities'
596
+ import * as authSchema from './generated/auth-schema'
597
+
598
+ if (!process.env.DATABASE_URL) {
599
+ throw new Error('DATABASE_URL environment variable is required')
600
+ }
601
+
602
+ export default defineConfig({
603
+ db: {
604
+ url: process.env.DATABASE_URL,
605
+ poolMin: 2,
606
+ poolMax: 10,
607
+ },
608
+ logging: {
609
+ level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',
610
+ name: '${r}',
611
+ },
612
+ entities: [Category, Article],
613
+ plugins: [
614
+ auth({ providers: ['email'], schema: authSchema }),
615
+ content({
616
+ locales: [{ code: 'en', label: 'English' }],
617
+ defaultLocale: 'en',
618
+ }),
619
+ logging(),
620
+ settings(),
621
+ storage(),
622
+ media(),
623
+ taxonomy(),
624
+ queue(),
625
+ mail({
626
+ provider: process.env.RESEND_API_KEY
627
+ ? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })
628
+ : undefined,
629
+ defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',
630
+ webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
631
+ }),
632
+ ticketing({
633
+ csatSecret: process.env.CSAT_SECRET,
634
+ }),
635
+ ],
636
+ projectRoot: import.meta.dirname,
637
+ })
638
+ `),await t((0,i.join)(o,"auth.config.ts"),`import { betterAuth } from 'better-auth'
639
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
640
+ import { admin } from 'better-auth/plugins'
641
+ import { organization } from 'better-auth/plugins/organization'
642
+ import { drizzle } from 'drizzle-orm/postgres-js'
643
+ import postgres from 'postgres'
644
+
645
+ const sql = postgres(process.env.DATABASE_URL!)
646
+ const db = drizzle(sql)
647
+
648
+ export const auth = betterAuth({
649
+ database: drizzleAdapter(db, { provider: 'pg' }),
650
+ emailAndPassword: { enabled: true },
651
+ plugins: [
652
+ admin(),
653
+ organization(),
654
+ ],
655
+ })
656
+ `),await t((0,i.join)(o,"drizzle.config.ts"),`import type { Config } from 'drizzle-kit'
657
+
658
+ if (!process.env.DATABASE_URL) {
659
+ throw new Error('DATABASE_URL environment variable is required')
660
+ }
661
+
662
+ export default {
663
+ schema: ['./generated/schema.ts', './generated/auth-schema.ts'],
664
+ out: './migrations',
665
+ dialect: 'postgresql',
666
+ dbCredentials: {
667
+ url: process.env.DATABASE_URL,
668
+ },
669
+ } satisfies Config
670
+ `),await t((0,i.join)(o,"entities/index.ts"),`export { Article } from './article'
671
+ export { Category } from './category'
672
+ `),await t((0,i.join)(o,"entities/article.ts"),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
673
+
674
+ export const Article = defineEntity({
675
+ name: 'article',
676
+ fields: {
677
+ title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),
678
+ slug: field.slug({ from: 'title', unique: true }),
679
+ excerpt: field.text({ maxLength: 500, translatable: true }),
680
+ body: field.richtext(),
681
+ viewCount: field.number({ default: 0, integer: true }),
682
+ featured: field.boolean({ default: false }),
683
+ publishDate: field.date(),
684
+ contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),
685
+ category: field.reference({ entity: 'category', required: false }),
686
+ tags: field.reference({ entity: 'category', cardinality: 'many' }),
687
+ coverImage: field.media({ accept: ['image/*'] }),
688
+ },
689
+ behaviors: [
690
+ behavior.publishable(),
691
+ behavior.auditable(),
692
+ behavior.sluggable('title'),
693
+ behavior.revisionable(),
694
+ ],
695
+ scope: 'global',
696
+ access: {
697
+ view: 'public',
698
+ create: 'group.editor',
699
+ update: 'group.editor',
700
+ delete: 'group.admin',
701
+ },
702
+ })
703
+ `),await t((0,i.join)(o,"entities/category.ts"),`import { defineEntity, field } from '@murumets-ee/entity'
704
+
705
+ export const Category = defineEntity({
706
+ name: 'category',
707
+ fields: {
708
+ name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),
709
+ slug: field.slug({ from: 'name', unique: true }),
710
+ description: field.text({ maxLength: 500, translatable: true }),
711
+ },
712
+ scope: 'global',
713
+ access: {
714
+ view: 'public',
715
+ create: 'group.editor',
716
+ update: 'group.editor',
717
+ delete: 'group.admin',
718
+ },
719
+ })
720
+ `),await t((0,i.join)(o,"lib/app.ts"),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
721
+ import config from '../toolkit.config'
722
+
723
+ let appInstance: ToolkitApp | null = null
724
+
725
+ export async function getToolkitApp(): Promise<ToolkitApp> {
726
+ if (!appInstance) {
727
+ console.log('Initializing toolkit app...')
728
+ appInstance = await createApp(config)
729
+ setApp(appInstance)
730
+ console.log('Toolkit app initialized')
731
+ }
732
+ return appInstance
733
+ }
734
+ `),await t((0,i.join)(o,"lib/auth.ts"),`import { getToolkitApp } from './app'
735
+ import { getAuth } from '@murumets-ee/auth'
736
+
737
+ export async function getAuthInstance() {
738
+ await getToolkitApp()
739
+ return getAuth()
740
+ }
741
+ `),await t((0,i.join)(o,"lib/auth-client.ts"),`import { createClient } from '@murumets-ee/auth/client'
742
+
743
+ export const authClient = createClient()
744
+ `),await t((0,i.join)(o,"generated/auth-schema.ts"),`// This file is generated by better-auth CLI.
745
+ // Run: npx @better-auth/cli generate --config auth.config.ts -y
746
+ export {}
747
+ `),await t((0,i.join)(o,"scripts/generate-schema.ts"),`/**
748
+ * Generate Drizzle schemas from entity definitions
749
+ */
750
+
751
+ import { mkdir, writeFile } from 'node:fs/promises'
752
+ import { join } from 'node:path'
753
+ import { generateSchemaCode, generateTranslationSchemaCode } from '@murumets-ee/entity'
754
+ import config from '../toolkit.config'
755
+
756
+ async function generateSchemas() {
757
+ console.log('Generating Drizzle schemas from entity definitions...')
758
+
759
+ const schemaDir = join(import.meta.dirname, '..', 'generated')
760
+ await mkdir(schemaDir, { recursive: true })
761
+
762
+ const imports: string[] = []
763
+ const schemas: string[] = []
764
+ let hasTranslations = false
765
+
766
+ for (const entity of config.entities) {
767
+ console.log(\` - Generating schema for \${entity.name}\`)
768
+
769
+ const schemaCode = generateSchemaCode(entity)
770
+ schemas.push(\`\\n// \${entity.name} table\`)
771
+ schemas.push(schemaCode)
772
+
773
+ const translationCode = generateTranslationSchemaCode(entity)
774
+ if (translationCode) {
775
+ hasTranslations = true
776
+ schemas.push(\`\\n// \${entity.name} translations\`)
777
+ schemas.push(translationCode)
778
+ }
779
+ }
780
+
781
+ const pgCoreTypes = ['pgTable', 'varchar', 'text', 'boolean', 'timestamp', 'integer', 'doublePrecision', 'jsonb', 'uuid', 'index']
782
+ if (hasTranslations) pgCoreTypes.push('unique')
783
+ const pgCoreImports = \`import { \${pgCoreTypes.join(', ')} } from 'drizzle-orm/pg-core'\\n\`
784
+ imports.push(pgCoreImports)
785
+
786
+ const schemaFile = join(schemaDir, 'schema.ts')
787
+ await writeFile(schemaFile, [...imports, ...schemas].join('\\n'))
788
+
789
+ console.log(\`\\nSchemas written to: \${schemaFile}\`)
790
+ console.log('Run \\\`pnpm db:migrate:generate\\\` to create migrations')
791
+ }
792
+
793
+ generateSchemas().catch(console.error)
794
+ `),await t((0,i.join)(o,"scripts/migrate.ts"),`/**
795
+ * Run pending migrations
796
+ */
797
+
798
+ import { createDbClient, runMigrations } from '@murumets-ee/db'
799
+
800
+ async function migrate() {
801
+ console.log('Running migrations...')
802
+
803
+ try {
804
+ if (!process.env.DATABASE_URL) {
805
+ throw new Error('DATABASE_URL environment variable is required')
806
+ }
807
+
808
+ const db = createDbClient({ url: process.env.DATABASE_URL })
809
+ await runMigrations(db, import.meta.dirname + '/..')
810
+
811
+ console.log('Migrations completed successfully')
812
+ process.exit(0)
813
+ } catch (error) {
814
+ console.error('Migration failed:', error)
815
+ process.exit(1)
816
+ }
817
+ }
818
+
819
+ migrate()
820
+ `),await t((0,i.join)(o,"scripts/reset-db.ts"),`/**
821
+ * Reset database (DROP all tables)
822
+ * WARNING: Only for development
823
+ */
824
+
825
+ import postgres from 'postgres'
826
+
827
+ async function resetDb() {
828
+ const DATABASE_URL = process.env.DATABASE_URL
829
+
830
+ if (!DATABASE_URL) {
831
+ throw new Error('DATABASE_URL not set')
832
+ }
833
+
834
+ console.warn('WARNING: This will DROP ALL TABLES')
835
+ console.log('Database:', DATABASE_URL.split('@')[1])
836
+
837
+ const sql = postgres(DATABASE_URL)
838
+
839
+ try {
840
+ await sql\`
841
+ DROP SCHEMA public CASCADE;
842
+ CREATE SCHEMA public;
843
+ GRANT ALL ON SCHEMA public TO PUBLIC;
844
+ \`
845
+
846
+ console.log('Database reset complete')
847
+ } finally {
848
+ await sql.end()
849
+ }
850
+ }
851
+
852
+ resetDb().catch(console.error)
853
+ `)}async function V(e,a){let{name:r}=a,o=(0,i.join)(e,"apps/admin");await(0,E.mkdir)((0,i.join)(e,"apps"),{recursive:!0}),u(`pnpm create next-app@${"16"} ${o} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await g(o,["app/page.tsx","app/page.module.css","app/fonts","README.md","pnpm-workspace.yaml"]),await d((0,i.join)(o,"package.json"),{dependencies:{[`@${r}/config`]:"workspace:*","@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"better-auth":`^${n.betterAuth}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`,"react-hook-form":`^${n.reactHookForm}`,"@hookform/resolvers":`^${n.hookformResolvers}`,zod:`^${n.zod}`},devDependencies:{"babel-plugin-react-compiler":n.babelReactCompiler}});let{appendToFile:c}=await Promise.resolve().then(()=>(l(),T));await c((0,i.join)(o,".gitignore"),`
854
+ # Environment
855
+ .env*
856
+ !.env.example
857
+ `),await t((0,i.join)(o,"next.config.ts"),`import type { NextConfig } from 'next'
858
+ import createNextIntlPlugin from 'next-intl/plugin'
859
+
860
+ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
861
+
862
+ const nextConfig: NextConfig = {
863
+ reactCompiler: true,
864
+ transpilePackages: [
865
+ '@${r}/config',
866
+ '@murumets-ee/core',
867
+ '@murumets-ee/entity',
868
+ '@murumets-ee/db',
869
+ '@murumets-ee/logging',
870
+ '@murumets-ee/auth-ui',
871
+ ],
872
+ serverExternalPackages: ['drizzle-orm', 'postgres'],
873
+ experimental: {
874
+ serverActions: {
875
+ bodySizeLimit: '2mb',
876
+ },
877
+ },
878
+ }
879
+
880
+ export default withNextIntl(nextConfig)
881
+ `),await t((0,i.join)(o,"proxy.ts"),`import { NextRequest, NextResponse } from 'next/server'
882
+ import createMiddleware from 'next-intl/middleware'
883
+ import { routing } from './i18n/routing'
884
+
885
+ const intlMiddleware = createMiddleware(routing)
886
+
887
+ const protectedPaths = ['/setup']
888
+
889
+ export function proxy(request: NextRequest) {
890
+ const { pathname } = request.nextUrl
891
+
892
+ const localePattern = new RegExp(\`^/(\${routing.locales.join('|')})\`)
893
+ const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
894
+
895
+ if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
896
+ const session = request.cookies.get('better-auth.session_token')
897
+ if (!session) {
898
+ const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
899
+ return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
900
+ }
901
+ }
902
+
903
+ return intlMiddleware(request)
904
+ }
905
+
906
+ export const config = {
907
+ matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
908
+ }
909
+ `),await t((0,i.join)(o,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
910
+
911
+ export const routing = defineRouting({
912
+ locales: ['en'],
913
+ defaultLocale: 'en',
914
+ })
915
+ `),await t((0,i.join)(o,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
916
+ import { hasLocale } from 'next-intl'
917
+ import { routing } from './routing'
918
+ import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
919
+
920
+ export default getRequestConfig(async ({ requestLocale }) => {
921
+ const requested = await requestLocale
922
+ const locale = hasLocale(routing.locales, requested)
923
+ ? requested
924
+ : routing.defaultLocale
925
+
926
+ const authMessages = await getAuthMessages(locale)
927
+
928
+ return {
929
+ locale,
930
+ messages: {
931
+ ...authMessages,
932
+ },
933
+ }
934
+ })
935
+ `),await t((0,i.join)(o,"app/globals.css"),`@import "tailwindcss";
936
+ @source "../node_modules/@murumets-ee/auth-ui/dist";
937
+
938
+ @custom-variant dark (&:where(.dark, .dark *));
939
+
940
+ :root {
941
+ --background: #ffffff;
942
+ --foreground: #171717;
943
+ }
944
+
945
+ @theme inline {
946
+ --color-background: var(--background);
947
+ --color-foreground: var(--foreground);
948
+ --font-sans: var(--font-geist-sans);
949
+ --font-mono: var(--font-geist-mono);
950
+ }
951
+
952
+ .dark {
953
+ --background: #0a0a0a;
954
+ --foreground: #ededed;
955
+ }
956
+
957
+ body {
958
+ background: var(--background);
959
+ color: var(--foreground);
960
+ font-family: Arial, Helvetica, sans-serif;
961
+ }
962
+ `),await t((0,i.join)(o,"app/theme-provider.tsx"),`'use client'
963
+
964
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
965
+ import type { ReactNode } from 'react'
966
+
967
+ export function ThemeProvider({ children }: { children: ReactNode }) {
968
+ return (
969
+ <NextThemesProvider
970
+ attribute="class"
971
+ defaultTheme="system"
972
+ enableSystem
973
+ disableTransitionOnChange
974
+ >
975
+ {children}
976
+ </NextThemesProvider>
977
+ )
978
+ }
979
+ `),await t((0,i.join)(o,"app/theme-toggle.tsx"),`'use client'
980
+
981
+ import { useTheme } from 'next-themes'
982
+ import { useState, useEffect } from 'react'
983
+ import { Sun, Moon } from 'lucide-react'
984
+
985
+ export function ThemeToggle() {
986
+ const [mounted, setMounted] = useState(false)
987
+ const { resolvedTheme, setTheme } = useTheme()
988
+
989
+ useEffect(() => { setMounted(true) }, [])
990
+
991
+ if (!mounted) {
992
+ return <div className="h-8 w-8" />
993
+ }
994
+
995
+ return (
996
+ <button
997
+ type="button"
998
+ onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
999
+ className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
1000
+ aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
1001
+ >
1002
+ {resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
1003
+ </button>
1004
+ )
1005
+ }
1006
+ `),await t((0,i.join)(o,"app/nav-header.tsx"),`'use client'
1007
+
1008
+ import Link from 'next/link'
1009
+ import { usePathname } from 'next/navigation'
1010
+ import { ThemeToggle } from './theme-toggle'
1011
+
1012
+ const links = [
1013
+ { href: '/', label: 'Home' },
1014
+ { href: '/auth/sign-in', label: 'Sign In' },
1015
+ { href: '/setup', label: 'Setup' },
1016
+ ]
1017
+
1018
+ export function NavHeader() {
1019
+ const pathname = usePathname()
1020
+
1021
+ function isActive(href: string) {
1022
+ return href === '/' ? pathname === '/' : pathname.startsWith(href)
1023
+ }
1024
+
1025
+ const linkClass = (href: string) =>
1026
+ \`px-3 py-1.5 rounded-md text-sm transition-colors \${
1027
+ isActive(href)
1028
+ ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
1029
+ : 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
1030
+ }\`
1031
+
1032
+ return (
1033
+ <header className="sticky top-0 z-50 backdrop-blur-md border-b bg-white/80 border-zinc-200 dark:bg-zinc-950/80 dark:border-zinc-800">
1034
+ <nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
1035
+ <span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
1036
+ ${r} Admin
1037
+ </span>
1038
+ {links.map(({ href, label }) => (
1039
+ <Link key={href} href={href} className={linkClass(href)}>
1040
+ {label}
1041
+ </Link>
1042
+ ))}
1043
+ <div className="ml-auto">
1044
+ <ThemeToggle />
1045
+ </div>
1046
+ </nav>
1047
+ </header>
1048
+ )
1049
+ }
1050
+ `),await t((0,i.join)(o,"app/layout.tsx"),`import './globals.css'
1051
+
1052
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1053
+ return children
1054
+ }
1055
+ `),await t((0,i.join)(o,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
1056
+ import { Geist, Geist_Mono } from 'next/font/google'
1057
+ import { NextIntlClientProvider } from 'next-intl'
1058
+ import { getMessages } from 'next-intl/server'
1059
+ import { notFound } from 'next/navigation'
1060
+ import { routing } from '@/i18n/routing'
1061
+ import { ThemeProvider } from '../theme-provider'
1062
+ import { NavHeader } from '../nav-header'
1063
+
1064
+ const geistSans = Geist({
1065
+ variable: '--font-geist-sans',
1066
+ subsets: ['latin'],
1067
+ })
1068
+
1069
+ const geistMono = Geist_Mono({
1070
+ variable: '--font-geist-mono',
1071
+ subsets: ['latin'],
1072
+ })
1073
+
1074
+ export const metadata: Metadata = {
1075
+ title: '${r} Admin',
1076
+ description: 'Built with Lumi CMS Toolkit',
1077
+ }
1078
+
1079
+ export default async function LocaleLayout({
1080
+ children,
1081
+ params,
1082
+ }: {
1083
+ children: React.ReactNode
1084
+ params: Promise<{ locale: string }>
1085
+ }) {
1086
+ const { locale } = await params
1087
+ if (!routing.locales.includes(locale as any)) notFound()
1088
+
1089
+ const messages = await getMessages()
1090
+
1091
+ return (
1092
+ <html lang={locale} suppressHydrationWarning>
1093
+ <body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
1094
+ <ThemeProvider>
1095
+ <NextIntlClientProvider locale={locale} messages={messages}>
1096
+ <NavHeader />
1097
+ {children}
1098
+ </NextIntlClientProvider>
1099
+ </ThemeProvider>
1100
+ </body>
1101
+ </html>
1102
+ )
1103
+ }
1104
+ `),await t((0,i.join)(o,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
1105
+ import { Article, Category } from '@${r}/config/entities'
1106
+ import { getToolkitApp } from '@${r}/config/app'
1107
+
1108
+ export default async function HomePage() {
1109
+ await getToolkitApp()
1110
+ const articles = createQueryClient(Article)
1111
+ const categories = createQueryClient(Category)
1112
+
1113
+ const [articleList, categoryList] = await Promise.all([
1114
+ articles.findMany({ limit: 10 }),
1115
+ categories.findMany({}),
1116
+ ])
1117
+
1118
+ return (
1119
+ <div className="min-h-screen p-8">
1120
+ <div className="max-w-4xl mx-auto">
1121
+ <h1 className="text-4xl font-bold mb-8">Welcome to ${r}</h1>
1122
+
1123
+ <section className="mb-12">
1124
+ <h2 className="text-2xl font-semibold mb-4">
1125
+ Categories ({categoryList.length})
1126
+ </h2>
1127
+ <div className="grid gap-4 md:grid-cols-2">
1128
+ {categoryList.map((cat) => (
1129
+ <div
1130
+ key={cat.id}
1131
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
1132
+ >
1133
+ <h3 className="font-semibold text-lg">{cat.name}</h3>
1134
+ <p className="text-sm text-zinc-600 dark:text-zinc-400">
1135
+ {cat.description}
1136
+ </p>
1137
+ </div>
1138
+ ))}
1139
+ </div>
1140
+ </section>
1141
+
1142
+ <section>
1143
+ <h2 className="text-2xl font-semibold mb-4">
1144
+ Articles ({articleList.length})
1145
+ </h2>
1146
+ {articleList.length === 0 ? (
1147
+ <p className="text-zinc-500">No published articles yet.</p>
1148
+ ) : (
1149
+ <div className="space-y-4">
1150
+ {articleList.map((article) => (
1151
+ <div
1152
+ key={article.id}
1153
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
1154
+ >
1155
+ <h3 className="text-xl font-bold">{article.title}</h3>
1156
+ <p className="text-zinc-600 dark:text-zinc-400 mt-2">
1157
+ {article.excerpt}
1158
+ </p>
1159
+ </div>
1160
+ ))}
1161
+ </div>
1162
+ )}
1163
+ </section>
1164
+ </div>
1165
+ </div>
1166
+ )
1167
+ }
1168
+ `),await t((0,i.join)(o,"app/api/auth/[...all]/route.ts"),`import { toNextJsHandler } from 'better-auth/next-js'
1169
+ import { getAuthInstance } from '@${r}/config/auth'
1170
+
1171
+ let _handler: ReturnType<typeof toNextJsHandler> | null = null
1172
+
1173
+ async function handler() {
1174
+ if (!_handler) {
1175
+ const auth = await getAuthInstance()
1176
+ _handler = toNextJsHandler(auth)
1177
+ }
1178
+ return _handler
1179
+ }
1180
+
1181
+ export async function GET(req: Request) {
1182
+ return (await handler()).GET(req)
1183
+ }
1184
+
1185
+ export async function POST(req: Request) {
1186
+ return (await handler()).POST(req)
1187
+ }
1188
+ `),await t((0,i.join)(o,"app/[locale]/auth/layout.tsx"),`import { AuthProviders } from './providers'
1189
+ import type { ReactNode } from 'react'
1190
+
1191
+ export default function AuthLayout({ children }: { children: ReactNode }) {
1192
+ return (
1193
+ <AuthProviders>
1194
+ <div className="flex min-h-[calc(100vh-3rem)] items-center justify-center px-4">
1195
+ {children}
1196
+ </div>
1197
+ </AuthProviders>
1198
+ )
1199
+ }
1200
+ `),await t((0,i.join)(o,"app/[locale]/auth/providers.tsx"),`'use client'
1201
+
1202
+ import { AuthUIProvider } from '@murumets-ee/auth-ui'
1203
+ import { authClient } from '@${r}/config/auth-client'
1204
+ import Link from 'next/link'
1205
+ import { useRouter } from 'next/navigation'
1206
+ import { useLocale } from 'next-intl'
1207
+ import type { ReactNode } from 'react'
1208
+
1209
+ export function AuthProviders({ children }: { children: ReactNode }) {
1210
+ const router = useRouter()
1211
+ const locale = useLocale()
1212
+
1213
+ return (
1214
+ <AuthUIProvider
1215
+ authClient={authClient as never}
1216
+ basePath="/auth"
1217
+ redirectTo="/"
1218
+ Link={Link}
1219
+ navigate={(url) => router.push(url)}
1220
+ resetPasswordUrl={\`/\${locale}/auth/reset-password\`}
1221
+ >
1222
+ {children}
1223
+ </AuthUIProvider>
1224
+ )
1225
+ }
1226
+ `),await t((0,i.join)(o,"app/[locale]/auth/sign-in/page.tsx"),`import { SignInForm } from '@murumets-ee/auth-ui'
1227
+
1228
+ export default function SignInPage() {
1229
+ return <SignInForm />
1230
+ }
1231
+ `),await t((0,i.join)(o,"app/[locale]/auth/sign-up/page.tsx"),`import { SignUpForm } from '@murumets-ee/auth-ui'
1232
+
1233
+ export default function SignUpPage() {
1234
+ return <SignUpForm />
1235
+ }
1236
+ `),await t((0,i.join)(o,"app/[locale]/auth/forgot-password/page.tsx"),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
1237
+
1238
+ export default function ForgotPasswordPage() {
1239
+ return <ForgotPasswordForm />
1240
+ }
1241
+ `),await t((0,i.join)(o,"app/[locale]/auth/reset-password/page.tsx"),`import { Suspense } from 'react'
1242
+ import { ResetPasswordContent } from './content'
1243
+
1244
+ export default function ResetPasswordPage() {
1245
+ return (
1246
+ <Suspense>
1247
+ <ResetPasswordContent />
1248
+ </Suspense>
1249
+ )
1250
+ }
1251
+ `),await t((0,i.join)(o,"app/[locale]/auth/reset-password/content.tsx"),`'use client'
1252
+
1253
+ import { useSearchParams } from 'next/navigation'
1254
+ import { ResetPasswordForm } from '@murumets-ee/auth-ui'
1255
+
1256
+ export function ResetPasswordContent() {
1257
+ const searchParams = useSearchParams()
1258
+ const token = searchParams.get('token')
1259
+
1260
+ if (!token) {
1261
+ return (
1262
+ <div className="text-center">
1263
+ <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
1264
+ Invalid or expired link
1265
+ </h1>
1266
+ <p className="mt-2 text-zinc-500">
1267
+ Please request a new password reset.
1268
+ </p>
1269
+ </div>
1270
+ )
1271
+ }
1272
+
1273
+ return <ResetPasswordForm token={token} />
1274
+ }
1275
+ `),await t((0,i.join)(o,"app/[locale]/setup/page.tsx"),`import { redirect } from 'next/navigation'
1276
+ import { sql } from 'drizzle-orm'
1277
+ import { getToolkitApp } from '@${r}/config/app'
1278
+ import { SetupForm } from './form'
1279
+
1280
+ export default async function SetupPage() {
1281
+ const app = await getToolkitApp()
1282
+ const result = await app.db.readOnly.execute<{ count: string }>(
1283
+ sql\`SELECT COUNT(*)::text as count FROM "user"\`,
1284
+ )
1285
+
1286
+ if (Number(result[0]?.count) > 0) {
1287
+ redirect('/')
1288
+ }
1289
+
1290
+ return (
1291
+ <div className="max-w-md mx-auto p-8">
1292
+ <h1 className="text-2xl font-bold mb-2">Create Admin</h1>
1293
+ <p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
1294
+ No users found. Create the first admin account.
1295
+ </p>
1296
+ <SetupForm />
1297
+ </div>
1298
+ )
1299
+ }
1300
+ `),await t((0,i.join)(o,"app/[locale]/setup/form.tsx"),`'use client'
1301
+
1302
+ import { useState } from 'react'
1303
+ import { createFirstAdmin } from './actions'
1304
+
1305
+ export function SetupForm() {
1306
+ const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)
1307
+ const [loading, setLoading] = useState(false)
1308
+
1309
+ async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
1310
+ e.preventDefault()
1311
+ setLoading(true)
1312
+ setResult(null)
1313
+ const res = await createFirstAdmin(new FormData(e.currentTarget))
1314
+ setResult(res)
1315
+ setLoading(false)
1316
+ }
1317
+
1318
+ if (result?.ok) {
1319
+ return (
1320
+ <div className="space-y-2">
1321
+ <p className="text-green-600 dark:text-green-400">Admin created: {result.email}</p>
1322
+ <a href="/auth/sign-in" className="text-blue-600 dark:text-blue-400 hover:underline">
1323
+ Sign in &rarr;
1324
+ </a>
1325
+ </div>
1326
+ )
1327
+ }
1328
+
1329
+ return (
1330
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
1331
+ <label className="text-sm font-medium">
1332
+ Name
1333
+ <input
1334
+ name="name"
1335
+ required
1336
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
1337
+ />
1338
+ </label>
1339
+ <label className="text-sm font-medium">
1340
+ Email
1341
+ <input
1342
+ name="email"
1343
+ type="email"
1344
+ required
1345
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
1346
+ />
1347
+ </label>
1348
+ <label className="text-sm font-medium">
1349
+ Password (min 8 chars)
1350
+ <input
1351
+ name="password"
1352
+ type="password"
1353
+ required
1354
+ minLength={8}
1355
+ className="mt-1 block w-full px-3 py-2 bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500"
1356
+ />
1357
+ </label>
1358
+
1359
+ {result?.error && <p className="text-red-600 dark:text-red-400 text-sm">{result.error}</p>}
1360
+
1361
+ <button
1362
+ type="submit"
1363
+ disabled={loading}
1364
+ className="px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors"
1365
+ >
1366
+ {loading ? 'Creating...' : 'Create Admin'}
1367
+ </button>
1368
+ </form>
1369
+ )
1370
+ }
1371
+ `),await t((0,i.join)(o,"app/[locale]/setup/actions.ts"),`'use server'
1372
+
1373
+ import { sql } from 'drizzle-orm'
1374
+ import { getToolkitApp } from '@${r}/config/app'
1375
+ import { getAuth } from '@murumets-ee/auth'
1376
+
1377
+ export async function createFirstAdmin(formData: FormData) {
1378
+ const email = formData.get('email') as string
1379
+ const password = formData.get('password') as string
1380
+ const name = formData.get('name') as string
1381
+
1382
+ if (!email || !password || !name) {
1383
+ return { error: 'All fields are required' }
1384
+ }
1385
+
1386
+ if (password.length < 8) {
1387
+ return { error: 'Password must be at least 8 characters' }
1388
+ }
1389
+
1390
+ const app = await getToolkitApp()
1391
+
1392
+ const canCreate = await app.db.readWrite.transaction(async (tx) => {
1393
+ await tx.execute(sql\`SELECT pg_advisory_xact_lock(1)\`)
1394
+
1395
+ const result = await tx.execute<{ count: string }>(
1396
+ sql\`SELECT COUNT(*)::text as count FROM "user"\`,
1397
+ )
1398
+ return Number(result[0]?.count) === 0
1399
+ })
1400
+
1401
+ if (!canCreate) {
1402
+ return { error: 'Admin user already exists. Setup is complete.' }
1403
+ }
1404
+
1405
+ const auth = getAuth()
1406
+
1407
+ const adminUser = await auth.api.signUpEmail({
1408
+ body: { email, password, name },
1409
+ })
1410
+
1411
+ await app.db.readWrite.execute(
1412
+ sql\`UPDATE "user" SET role = 'admin' WHERE id = \${adminUser.user.id}\`,
1413
+ )
1414
+
1415
+ return { ok: true, email: adminUser.user.email }
1416
+ }
1417
+ `)}async function J(e,a){let{name:r}=a,o=(0,i.join)(e,"apps/web");u(`pnpm create next-app@${"16"} ${o} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await g(o,["app/page.tsx","app/page.module.css","app/fonts","README.md","pnpm-workspace.yaml"]),await d((0,i.join)(o,"package.json"),{dependencies:{[`@${r}/config`]:"workspace:*","@murumets-ee/core":`^${n.myorgCore}`,"@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`},devDependencies:{"babel-plugin-react-compiler":n.babelReactCompiler}});let{appendToFile:c}=await Promise.resolve().then(()=>(l(),T));await c((0,i.join)(o,".gitignore"),`
1418
+ # Environment
1419
+ .env*
1420
+ !.env.example
1421
+ `),await t((0,i.join)(o,"next.config.ts"),`import type { NextConfig } from 'next'
1422
+ import createNextIntlPlugin from 'next-intl/plugin'
1423
+
1424
+ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
1425
+
1426
+ const nextConfig: NextConfig = {
1427
+ reactCompiler: true,
1428
+ transpilePackages: [
1429
+ '@${r}/config',
1430
+ '@murumets-ee/core',
1431
+ '@murumets-ee/entity',
1432
+ '@murumets-ee/db',
1433
+ '@murumets-ee/logging',
1434
+ '@murumets-ee/auth-ui',
1435
+ ],
1436
+ serverExternalPackages: ['drizzle-orm', 'postgres'],
1437
+ }
1438
+
1439
+ export default withNextIntl(nextConfig)
1440
+ `),await t((0,i.join)(o,"proxy.ts"),`import createMiddleware from 'next-intl/middleware'
1441
+ import { routing } from './i18n/routing'
1442
+
1443
+ export default createMiddleware(routing)
1444
+
1445
+ export const config = {
1446
+ matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
1447
+ }
1448
+ `),await t((0,i.join)(o,"i18n/routing.ts"),`import { defineRouting } from 'next-intl/routing'
1449
+
1450
+ export const routing = defineRouting({
1451
+ locales: ['en'],
1452
+ defaultLocale: 'en',
1453
+ })
1454
+ `),await t((0,i.join)(o,"i18n/request.ts"),`import { getRequestConfig } from 'next-intl/server'
1455
+ import { hasLocale } from 'next-intl'
1456
+ import { routing } from './routing'
1457
+ import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
1458
+
1459
+ export default getRequestConfig(async ({ requestLocale }) => {
1460
+ const requested = await requestLocale
1461
+ const locale = hasLocale(routing.locales, requested)
1462
+ ? requested
1463
+ : routing.defaultLocale
1464
+
1465
+ const authMessages = await getAuthMessages(locale)
1466
+
1467
+ return {
1468
+ locale,
1469
+ messages: {
1470
+ ...authMessages,
1471
+ },
1472
+ }
1473
+ })
1474
+ `),await t((0,i.join)(o,"app/globals.css"),`@import "tailwindcss";
1475
+ @source "../node_modules/@murumets-ee/auth-ui/dist";
1476
+
1477
+ @custom-variant dark (&:where(.dark, .dark *));
1478
+
1479
+ :root {
1480
+ --background: #ffffff;
1481
+ --foreground: #171717;
1482
+ }
1483
+
1484
+ @theme inline {
1485
+ --color-background: var(--background);
1486
+ --color-foreground: var(--foreground);
1487
+ --font-sans: var(--font-geist-sans);
1488
+ --font-mono: var(--font-geist-mono);
1489
+ }
1490
+
1491
+ .dark {
1492
+ --background: #0a0a0a;
1493
+ --foreground: #ededed;
1494
+ }
1495
+
1496
+ body {
1497
+ background: var(--background);
1498
+ color: var(--foreground);
1499
+ font-family: Arial, Helvetica, sans-serif;
1500
+ }
1501
+ `),await t((0,i.join)(o,"app/theme-provider.tsx"),`'use client'
1502
+
1503
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
1504
+ import type { ReactNode } from 'react'
1505
+
1506
+ export function ThemeProvider({ children }: { children: ReactNode }) {
1507
+ return (
1508
+ <NextThemesProvider
1509
+ attribute="class"
1510
+ defaultTheme="system"
1511
+ enableSystem
1512
+ disableTransitionOnChange
1513
+ >
1514
+ {children}
1515
+ </NextThemesProvider>
1516
+ )
1517
+ }
1518
+ `),await t((0,i.join)(o,"app/theme-toggle.tsx"),`'use client'
1519
+
1520
+ import { useTheme } from 'next-themes'
1521
+ import { useState, useEffect } from 'react'
1522
+ import { Sun, Moon } from 'lucide-react'
1523
+
1524
+ export function ThemeToggle() {
1525
+ const [mounted, setMounted] = useState(false)
1526
+ const { resolvedTheme, setTheme } = useTheme()
1527
+
1528
+ useEffect(() => { setMounted(true) }, [])
1529
+
1530
+ if (!mounted) {
1531
+ return <div className="h-8 w-8" />
1532
+ }
1533
+
1534
+ return (
1535
+ <button
1536
+ type="button"
1537
+ onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
1538
+ className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
1539
+ aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
1540
+ >
1541
+ {resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
1542
+ </button>
1543
+ )
1544
+ }
1545
+ `),await t((0,i.join)(o,"app/nav-header.tsx"),`'use client'
1546
+
1547
+ import Link from 'next/link'
1548
+ import { usePathname } from 'next/navigation'
1549
+ import { ThemeToggle } from './theme-toggle'
1550
+
1551
+ const links = [
1552
+ { href: '/', label: 'Home' },
1553
+ ]
1554
+
1555
+ export function NavHeader() {
1556
+ const pathname = usePathname()
1557
+
1558
+ function isActive(href: string) {
1559
+ return href === '/' ? pathname === '/' : pathname.startsWith(href)
1560
+ }
1561
+
1562
+ const linkClass = (href: string) =>
1563
+ \`px-3 py-1.5 rounded-md text-sm transition-colors \${
1564
+ isActive(href)
1565
+ ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
1566
+ : 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
1567
+ }\`
1568
+
1569
+ return (
1570
+ <header className="sticky top-0 z-50 backdrop-blur-md border-b bg-white/80 border-zinc-200 dark:bg-zinc-950/80 dark:border-zinc-800">
1571
+ <nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
1572
+ <span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
1573
+ ${r}
1574
+ </span>
1575
+ {links.map(({ href, label }) => (
1576
+ <Link key={href} href={href} className={linkClass(href)}>
1577
+ {label}
1578
+ </Link>
1579
+ ))}
1580
+ <div className="ml-auto">
1581
+ <ThemeToggle />
1582
+ </div>
1583
+ </nav>
1584
+ </header>
1585
+ )
1586
+ }
1587
+ `),await t((0,i.join)(o,"app/layout.tsx"),`import './globals.css'
1588
+
1589
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1590
+ return children
1591
+ }
1592
+ `),await t((0,i.join)(o,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
1593
+ import { Geist, Geist_Mono } from 'next/font/google'
1594
+ import { NextIntlClientProvider } from 'next-intl'
1595
+ import { getMessages } from 'next-intl/server'
1596
+ import { notFound } from 'next/navigation'
1597
+ import { routing } from '@/i18n/routing'
1598
+ import { ThemeProvider } from '../theme-provider'
1599
+ import { NavHeader } from '../nav-header'
1600
+
1601
+ const geistSans = Geist({
1602
+ variable: '--font-geist-sans',
1603
+ subsets: ['latin'],
1604
+ })
1605
+
1606
+ const geistMono = Geist_Mono({
1607
+ variable: '--font-geist-mono',
1608
+ subsets: ['latin'],
1609
+ })
1610
+
1611
+ export const metadata: Metadata = {
1612
+ title: '${r}',
1613
+ description: 'Built with Lumi CMS Toolkit',
1614
+ }
1615
+
1616
+ export default async function LocaleLayout({
1617
+ children,
1618
+ params,
1619
+ }: {
1620
+ children: React.ReactNode
1621
+ params: Promise<{ locale: string }>
1622
+ }) {
1623
+ const { locale } = await params
1624
+ if (!routing.locales.includes(locale as any)) notFound()
1625
+
1626
+ const messages = await getMessages()
1627
+
1628
+ return (
1629
+ <html lang={locale} suppressHydrationWarning>
1630
+ <body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
1631
+ <ThemeProvider>
1632
+ <NextIntlClientProvider locale={locale} messages={messages}>
1633
+ <NavHeader />
1634
+ {children}
1635
+ </NextIntlClientProvider>
1636
+ </ThemeProvider>
1637
+ </body>
1638
+ </html>
1639
+ )
1640
+ }
1641
+ `),await t((0,i.join)(o,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
1642
+ import { Article, Category } from '@${r}/config/entities'
1643
+ import { getToolkitApp } from '@${r}/config/app'
1644
+
1645
+ export default async function HomePage() {
1646
+ await getToolkitApp()
1647
+ const articles = createQueryClient(Article)
1648
+ const categories = createQueryClient(Category)
1649
+
1650
+ const [articleList, categoryList] = await Promise.all([
1651
+ articles.findMany({ limit: 10 }),
1652
+ categories.findMany({}),
1653
+ ])
1654
+
1655
+ return (
1656
+ <div className="min-h-screen p-8">
1657
+ <div className="max-w-4xl mx-auto">
1658
+ <h1 className="text-4xl font-bold mb-8">Welcome to ${r}</h1>
1659
+
1660
+ <section className="mb-12">
1661
+ <h2 className="text-2xl font-semibold mb-4">
1662
+ Categories ({categoryList.length})
1663
+ </h2>
1664
+ <div className="grid gap-4 md:grid-cols-2">
1665
+ {categoryList.map((cat) => (
1666
+ <div
1667
+ key={cat.id}
1668
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
1669
+ >
1670
+ <h3 className="font-semibold text-lg">{cat.name}</h3>
1671
+ <p className="text-sm text-zinc-600 dark:text-zinc-400">
1672
+ {cat.description}
1673
+ </p>
1674
+ </div>
1675
+ ))}
1676
+ </div>
1677
+ </section>
1678
+
1679
+ <section>
1680
+ <h2 className="text-2xl font-semibold mb-4">
1681
+ Articles ({articleList.length})
1682
+ </h2>
1683
+ {articleList.length === 0 ? (
1684
+ <p className="text-zinc-500">No published articles yet.</p>
1685
+ ) : (
1686
+ <div className="space-y-4">
1687
+ {articleList.map((article) => (
1688
+ <div
1689
+ key={article.id}
1690
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
1691
+ >
1692
+ <h3 className="text-xl font-bold">{article.title}</h3>
1693
+ <p className="text-zinc-600 dark:text-zinc-400 mt-2">
1694
+ {article.excerpt}
1695
+ </p>
1696
+ </div>
1697
+ ))}
1698
+ </div>
1699
+ )}
1700
+ </section>
1701
+ </div>
1702
+ </div>
1703
+ )
1704
+ }
1705
+ `)}var f=require("path");l();async function M(e,a){let{name:r}=a;await t((0,f.join)(e,"next.config.ts"),`import type { NextConfig } from 'next'
1706
+ import createNextIntlPlugin from 'next-intl/plugin'
1707
+
1708
+ const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
1709
+
1710
+ const nextConfig: NextConfig = {
1711
+ output: 'standalone',
1712
+ reactCompiler: true,
1713
+ transpilePackages: [
1714
+ '@murumets-ee/core', '@murumets-ee/entity', '@murumets-ee/db',
1715
+ '@murumets-ee/logging', '@murumets-ee/auth', '@murumets-ee/auth-ui',
1716
+ '@murumets-ee/admin-ui', '@murumets-ee/content', '@murumets-ee/settings',
1717
+ '@murumets-ee/storage', '@murumets-ee/media', '@murumets-ee/taxonomy',
1718
+ '@murumets-ee/queue', '@murumets-ee/mail', '@murumets-ee/ticketing',
1719
+ '@murumets-ee/ticketing-ui', '@murumets-ee/tokens', '@murumets-ee/ui',
1720
+ ],
1721
+ serverExternalPackages: ['drizzle-orm', 'postgres', 'pino', 'file-type'],
1722
+ experimental: {
1723
+ serverActions: {
1724
+ bodySizeLimit: '2mb',
1725
+ },
1726
+ },
1727
+ }
1728
+
1729
+ export default withNextIntl(nextConfig)
1730
+ `),await t((0,f.join)(e,"proxy.ts"),`import { NextRequest, NextResponse } from 'next/server'
1731
+ import createMiddleware from 'next-intl/middleware'
1732
+ import { routing } from './i18n/routing'
1733
+
1734
+ const intlMiddleware = createMiddleware(routing)
1735
+
1736
+ const protectedPaths = ['/setup']
1737
+
1738
+ export function proxy(request: NextRequest) {
1739
+ const { pathname } = request.nextUrl
1740
+
1741
+ // Strip locale prefix to check actual path
1742
+ const localePattern = new RegExp(\`^/(\${routing.locales.join('|')})\`)
1743
+ const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
1744
+
1745
+ // Auth check for protected paths
1746
+ if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
1747
+ const session = request.cookies.get('better-auth.session_token')
1748
+ if (!session) {
1749
+ const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
1750
+ return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
1751
+ }
1752
+ }
1753
+
1754
+ // Locale routing
1755
+ return intlMiddleware(request)
1756
+ }
1757
+
1758
+ export const config = {
1759
+ matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
1760
+ }
1761
+ `),await t((0,f.join)(e,"app/layout.tsx"),`import './globals.css'
1762
+
1763
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
1764
+ return children
1765
+ }
1766
+ `),await t((0,f.join)(e,"app/[locale]/layout.tsx"),`import type { Metadata } from 'next'
1767
+ import { Geist, Geist_Mono } from 'next/font/google'
1768
+ import { NextIntlClientProvider } from 'next-intl'
1769
+ import { getMessages } from 'next-intl/server'
1770
+ import { notFound } from 'next/navigation'
1771
+ import { routing } from '@/i18n/routing'
1772
+ import { ThemeProvider } from '../theme-provider'
1773
+ import { NavHeader } from '../nav-header'
1774
+
1775
+ const geistSans = Geist({
1776
+ variable: '--font-geist-sans',
1777
+ subsets: ['latin'],
1778
+ })
1779
+
1780
+ const geistMono = Geist_Mono({
1781
+ variable: '--font-geist-mono',
1782
+ subsets: ['latin'],
1783
+ })
1784
+
1785
+ export const metadata: Metadata = {
1786
+ title: '${r}',
1787
+ description: 'Built with Lumi CMS Toolkit',
1788
+ }
1789
+
1790
+ export default async function LocaleLayout({
1791
+ children,
1792
+ params,
1793
+ }: {
1794
+ children: React.ReactNode
1795
+ params: Promise<{ locale: string }>
1796
+ }) {
1797
+ const { locale } = await params
1798
+ if (!routing.locales.includes(locale as any)) notFound()
1799
+
1800
+ const messages = await getMessages()
1801
+
1802
+ return (
1803
+ <html lang={locale} suppressHydrationWarning>
1804
+ <body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
1805
+ <ThemeProvider>
1806
+ <NextIntlClientProvider locale={locale} messages={messages}>
1807
+ <NavHeader />
1808
+ {children}
1809
+ </NextIntlClientProvider>
1810
+ </ThemeProvider>
1811
+ </body>
1812
+ </html>
1813
+ )
1814
+ }
1815
+ `),await t((0,f.join)(e,"app/[locale]/page.tsx"),`import { createQueryClient } from '@murumets-ee/core/clients'
1816
+ import { Article, Category } from '@/entities'
1817
+ import { getToolkitApp } from '@/lib/app'
1818
+
1819
+ export default async function HomePage() {
1820
+ await getToolkitApp()
1821
+ const articles = createQueryClient(Article)
1822
+ const categories = createQueryClient(Category)
1823
+
1824
+ const [articleList, categoryList] = await Promise.all([
1825
+ articles.findMany({ limit: 10 }),
1826
+ categories.findMany({}),
1827
+ ])
1828
+
1829
+ return (
1830
+ <div className="min-h-screen p-8">
1831
+ <div className="max-w-4xl mx-auto">
1832
+ <h1 className="text-4xl font-bold mb-8">Welcome to ${r}</h1>
1833
+
1834
+ <section className="mb-12">
1835
+ <h2 className="text-2xl font-semibold mb-4">
1836
+ Categories ({categoryList.length})
1837
+ </h2>
1838
+ <div className="grid gap-4 md:grid-cols-2">
1839
+ {categoryList.map((cat) => (
1840
+ <div
1841
+ key={cat.id}
1842
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
1843
+ >
1844
+ <h3 className="font-semibold text-lg">{cat.name}</h3>
1845
+ <p className="text-sm text-zinc-600 dark:text-zinc-400">
1846
+ {cat.description}
1847
+ </p>
1848
+ </div>
1849
+ ))}
1850
+ </div>
1851
+ </section>
1852
+
1853
+ <section>
1854
+ <h2 className="text-2xl font-semibold mb-4">
1855
+ Articles ({articleList.length})
1856
+ </h2>
1857
+ {articleList.length === 0 ? (
1858
+ <p className="text-zinc-500">No published articles yet.</p>
1859
+ ) : (
1860
+ <div className="space-y-4">
1861
+ {articleList.map((article) => (
1862
+ <div
1863
+ key={article.id}
1864
+ className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
1865
+ >
1866
+ <h3 className="text-xl font-bold">{article.title}</h3>
1867
+ <p className="text-zinc-600 dark:text-zinc-400 mt-2">
1868
+ {article.excerpt}
1869
+ </p>
1870
+ </div>
1871
+ ))}
1872
+ </div>
1873
+ )}
1874
+ </section>
1875
+ </div>
1876
+ </div>
1877
+ )
1878
+ }
1879
+ `)}var w=require("path");l();async function O(e,a){await t((0,w.join)(e,"scripts/generate-schema.ts"),`import { execSync } from 'node:child_process'
1880
+ execSync('npx @murumets-ee/cli generate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
1881
+ `),await t((0,w.join)(e,"scripts/migrate.ts"),`import { execSync } from 'node:child_process'
1882
+ execSync('npx @murumets-ee/cli migrate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
1883
+ `),await t((0,w.join)(e,"scripts/reset-db.ts"),`import { execSync } from 'node:child_process'
1884
+ execSync('npx @murumets-ee/cli reset --force', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })
1885
+ `)}var x=require("path");l();async function q(e,a){let{name:r}=a;await t((0,x.join)(e,"app/globals.css"),`@import "tailwindcss";
1886
+ @source "../node_modules/@murumets-ee/auth-ui/dist";
1887
+
1888
+ @custom-variant dark (&:where(.dark, .dark *));
1889
+
1890
+ :root {
1891
+ --background: #ffffff;
1892
+ --foreground: #171717;
1893
+ }
1894
+
1895
+ @theme inline {
1896
+ --color-background: var(--background);
1897
+ --color-foreground: var(--foreground);
1898
+ --font-sans: var(--font-geist-sans);
1899
+ --font-mono: var(--font-geist-mono);
1900
+ }
1901
+
1902
+ .dark {
1903
+ --background: #0a0a0a;
1904
+ --foreground: #ededed;
1905
+ }
1906
+
1907
+ body {
1908
+ background: var(--background);
1909
+ color: var(--foreground);
1910
+ font-family: Arial, Helvetica, sans-serif;
1911
+ }
1912
+ `),await t((0,x.join)(e,"app/theme-provider.tsx"),`'use client'
1913
+
1914
+ import { ThemeProvider as NextThemesProvider } from 'next-themes'
1915
+ import type { ReactNode } from 'react'
1916
+
1917
+ export function ThemeProvider({ children }: { children: ReactNode }) {
1918
+ return (
1919
+ <NextThemesProvider
1920
+ attribute="class"
1921
+ defaultTheme="system"
1922
+ enableSystem
1923
+ disableTransitionOnChange
1924
+ >
1925
+ {children}
1926
+ </NextThemesProvider>
1927
+ )
1928
+ }
1929
+ `),await t((0,x.join)(e,"app/theme-toggle.tsx"),`'use client'
1930
+
1931
+ import { useTheme } from 'next-themes'
1932
+ import { useState, useEffect } from 'react'
1933
+ import { Sun, Moon } from 'lucide-react'
1934
+
1935
+ export function ThemeToggle() {
1936
+ const [mounted, setMounted] = useState(false)
1937
+ const { resolvedTheme, setTheme } = useTheme()
1938
+
1939
+ useEffect(() => { setMounted(true) }, [])
1940
+
1941
+ if (!mounted) {
1942
+ return <div className="h-8 w-8" />
1943
+ }
1944
+
1945
+ return (
1946
+ <button
1947
+ type="button"
1948
+ onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
1949
+ className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-200 cursor-pointer"
1950
+ aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
1951
+ >
1952
+ {resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
1953
+ </button>
1954
+ )
1955
+ }
1956
+ `),await t((0,x.join)(e,"app/nav-header.tsx"),`'use client'
1957
+
1958
+ import Link from 'next/link'
1959
+ import { usePathname } from 'next/navigation'
1960
+ import { ThemeToggle } from './theme-toggle'
1961
+
1962
+ const links = [
1963
+ { href: '/', label: 'Home' },
1964
+ { href: '/auth/sign-in', label: 'Sign In' },
1965
+ { href: '/setup', label: 'Setup' },
1966
+ ]
1967
+
1968
+ export function NavHeader() {
1969
+ const pathname = usePathname()
1970
+
1971
+ function isActive(href: string) {
1972
+ return href === '/' ? pathname === '/' : pathname.startsWith(href)
1973
+ }
1974
+
1975
+ const linkClass = (href: string) =>
1976
+ \`px-3 py-1.5 rounded-md text-sm transition-colors \${
1977
+ isActive(href)
1978
+ ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
1979
+ : 'text-zinc-500 hover:text-zinc-700 hover:bg-zinc-100 dark:text-zinc-400 dark:hover:text-zinc-200 dark:hover:bg-zinc-800/50'
1980
+ }\`
1981
+
1982
+ return (
1983
+ <header className="sticky top-0 z-50 backdrop-blur-md border-b bg-white/80 border-zinc-200 dark:bg-zinc-950/80 dark:border-zinc-800">
1984
+ <nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
1985
+ <span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
1986
+ ${r}
1987
+ </span>
1988
+ {links.map(({ href, label }) => (
1989
+ <Link key={href} href={href} className={linkClass(href)}>
1990
+ {label}
1991
+ </Link>
1992
+ ))}
1993
+ <div className="ml-auto">
1994
+ <ThemeToggle />
1995
+ </div>
1996
+ </nav>
1997
+ </header>
1998
+ )
1999
+ }
2000
+ `)}var h=require("path");l();async function I(e,a){let{name:r}=a;await t((0,h.join)(e,"toolkit.config.ts"),`import { auth } from '@murumets-ee/auth/plugin'
2001
+ import { content } from '@murumets-ee/content/plugin'
2002
+ import { defineConfig } from '@murumets-ee/core'
2003
+ import { logging } from '@murumets-ee/logging/plugin'
2004
+ import { mail, ResendMailProvider } from '@murumets-ee/mail'
2005
+ import { media } from '@murumets-ee/media/plugin'
2006
+ import { queue } from '@murumets-ee/queue/plugin'
2007
+ import { settings } from '@murumets-ee/settings/plugin'
2008
+ import { storage } from '@murumets-ee/storage/plugin'
2009
+ import { taxonomy } from '@murumets-ee/taxonomy/plugin'
2010
+ import { ticketing } from '@murumets-ee/ticketing/plugin'
2011
+ import { Article, Category } from './entities'
2012
+ import * as authSchema from './generated/auth-schema'
2013
+
2014
+ if (!process.env.DATABASE_URL) {
2015
+ throw new Error('DATABASE_URL environment variable is required')
2016
+ }
2017
+
2018
+ export default defineConfig({
2019
+ db: {
2020
+ url: process.env.DATABASE_URL,
2021
+ poolMin: 2,
2022
+ poolMax: 10,
2023
+ },
2024
+ logging: {
2025
+ level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',
2026
+ name: '${r}',
2027
+ },
2028
+ entities: [Category, Article],
2029
+ plugins: [
2030
+ auth({ providers: ['email'], schema: authSchema }),
2031
+ content({
2032
+ locales: [{ code: 'en', label: 'English' }],
2033
+ defaultLocale: 'en',
2034
+ }),
2035
+ logging(),
2036
+ settings(),
2037
+ storage(),
2038
+ media(),
2039
+ taxonomy(),
2040
+ queue(),
2041
+ mail({
2042
+ provider: process.env.RESEND_API_KEY
2043
+ ? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })
2044
+ : undefined,
2045
+ defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',
2046
+ webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
2047
+ }),
2048
+ ticketing({
2049
+ csatSecret: process.env.CSAT_SECRET,
2050
+ }),
2051
+ ],
2052
+ projectRoot: import.meta.dirname,
2053
+ })
2054
+ `),await t((0,h.join)(e,"lib/app.ts"),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
2055
+ import config from '../toolkit.config'
2056
+
2057
+ let appInstance: ToolkitApp | null = null
2058
+
2059
+ export async function getToolkitApp(): Promise<ToolkitApp> {
2060
+ if (!appInstance) {
2061
+ console.log('Initializing toolkit app...')
2062
+ appInstance = await createApp(config)
2063
+ setApp(appInstance)
2064
+ console.log('Toolkit app initialized')
2065
+ }
2066
+ return appInstance
2067
+ }
2068
+ `),await t((0,h.join)(e,"drizzle.config.ts"),`import type { Config } from 'drizzle-kit'
2069
+
2070
+ if (!process.env.DATABASE_URL) {
2071
+ throw new Error('DATABASE_URL environment variable is required')
2072
+ }
2073
+
2074
+ export default {
2075
+ schema: ['./generated/schema.ts', './generated/auth-schema.ts'],
2076
+ out: './migrations',
2077
+ dialect: 'postgresql',
2078
+ dbCredentials: {
2079
+ url: process.env.DATABASE_URL,
2080
+ },
2081
+ } satisfies Config
2082
+ `),await t((0,h.join)(e,"auth.config.ts"),`import { betterAuth } from 'better-auth'
2083
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle'
2084
+ import { admin } from 'better-auth/plugins'
2085
+ import { organization } from 'better-auth/plugins/organization'
2086
+ import { drizzle } from 'drizzle-orm/postgres-js'
2087
+ import postgres from 'postgres'
2088
+
2089
+ const sql = postgres(process.env.DATABASE_URL!)
2090
+ const db = drizzle(sql)
2091
+
2092
+ export const auth = betterAuth({
2093
+ database: drizzleAdapter(db, { provider: 'pg' }),
2094
+ emailAndPassword: { enabled: true },
2095
+ plugins: [
2096
+ admin(),
2097
+ organization(),
2098
+ ],
2099
+ })
2100
+ `),await t((0,h.join)(e,"generated/auth-schema.ts"),`// This file is generated by better-auth CLI.
2101
+ // Run: npx @better-auth/cli generate --config auth.config.ts -y
2102
+ export {}
2103
+ `)}l();async function U(e,a){e.mode==="single"?await Y(e,a):await Q(e,a)}async function Y(e,a){let r=(0,k.join)(process.cwd(),e.name);a?.("Creating Next.js app..."),u(`pnpm create next-app@${"16"} ${e.name} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await g(r,["app/page.tsx","app/page.module.css","app/fonts","README.md"]),await d((0,k.join)(r,"package.json"),{type:"module",dependencies:{"@murumets-ee/core":`^${n.myorgCore}`,"@murumets-ee/db":`^${n.myorgDb}`,"@murumets-ee/entity":`^${n.myorgEntity}`,"@murumets-ee/logging":`^${n.myorgLogging}`,"@murumets-ee/auth":`^${n.myorgAuth}`,"@murumets-ee/auth-ui":`^${n.myorgAuthUi}`,"@murumets-ee/admin-ui":`^${n.myorgAdminUi}`,"@murumets-ee/content":`^${n.myorgContent}`,"@murumets-ee/settings":`^${n.myorgSettings}`,"@murumets-ee/storage":`^${n.myorgStorage}`,"@murumets-ee/media":`^${n.myorgMedia}`,"@murumets-ee/taxonomy":`^${n.myorgTaxonomy}`,"@murumets-ee/queue":`^${n.myorgQueue}`,"@murumets-ee/mail":`^${n.myorgMail}`,"@murumets-ee/ticketing":`^${n.myorgTicketing}`,"@murumets-ee/ticketing-ui":`^${n.myorgTicketingUi}`,"@murumets-ee/tokens":`^${n.myorgTokens}`,"better-auth":`^${n.betterAuth}`,"drizzle-orm":`^${n.drizzleOrm}`,"next-intl":`^${n.nextIntl}`,"next-themes":`^${n.nextThemes}`,"lucide-react":`^${n.lucideReact}`,postgres:`^${n.postgres}`,"react-hook-form":`^${n.reactHookForm}`,"@hookform/resolvers":`^${n.hookformResolvers}`,zod:`^${n.zod}`},devDependencies:{"drizzle-kit":`^${n.drizzleKit}`,tsx:`^${n.tsx}`,"babel-plugin-react-compiler":n.babelReactCompiler},scripts:{"db:generate":"tsx --env-file=.env scripts/generate-schema.ts","db:migrate:generate":"drizzle-kit generate","db:migrate":"tsx --env-file=.env scripts/migrate.ts","db:reset":"tsx --env-file=.env scripts/reset-db.ts"}}),a?.("Adding toolkit files..."),await C(r,e),await L(r,e),await I(r,e),await z(r,e),await _(r,e),await M(r,e),await q(r,e),await O(r,e),e.installDeps&&(a?.("Installing dependencies..."),u("pnpm install",{cwd:r}))}async function Q(e,a){let r=(0,k.join)(process.cwd(),e.name);a?.("Creating workspace..."),await $(r,e,a),e.installDeps&&(a?.("Installing dependencies..."),u("pnpm install",{cwd:r}))}0&&(module.exports={scaffold});