@murumets-ee/create 0.1.12 → 0.1.14

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 @@
1
+ {"version":3,"file":"index.mjs","names":["writeFile","writeFile","writeFile","writeFile","writeFile","writeFile","writeFile","writeFile","writeFile","writeFile"],"sources":["../src/generators/admin.ts","../src/generators/auth.ts","../src/generators/base.ts","../src/generators/entities.ts","../src/generators/intl.ts","../src/versions.ts","../src/generators/multi.ts","../src/generators/nextjs.ts","../src/generators/scripts.ts","../src/generators/theme.ts","../src/generators/toolkit.ts","../src/scaffold.ts"],"sourcesContent":["import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\n/**\n * Generate a complete admin UI with:\n * - Admin shell layout (sidebar + header)\n * - Dynamic entity CRUD pages (list/create/edit)\n * - Media management pages\n * - Users, roles, permissions management\n * - Activity log\n * - Admin API route handler\n * - Supporting lib files (admin-config, withAdminContext, loadRoles, etc.)\n */\nexport async function generateAdmin(\n projectDir: string,\n _options: ProjectOptions,\n): Promise<void> {\n // =========================================================================\n // Lib files\n // =========================================================================\n\n await writeFile(\n join(projectDir, 'lib/admin-config.ts'),\n `import { Media } from '@murumets-ee/media'\nimport { Article, Category } from '@/entities'\n\n/** All entities available in the admin (drives sidebar nav + CRUD) */\nexport const allEntities = [Article, Media, Category]\n\n/** Taxonomy entities keyed by name */\nexport const taxonomyVocabularies = { category: Category }\n\n/** Entities exposed via generic CRUD API handler */\nexport const crudEntities = [Article, Media]\n\n/** Plugin resources for the permission catalog */\nexport const pluginResources = [\n { resource: 'storage', actions: ['view', 'create', 'update', 'delete'] },\n { resource: 'settings', actions: ['view', 'update'] },\n { resource: 'audit-logs', actions: ['view'] },\n { resource: 'permissions', actions: ['view', 'create', 'update', 'delete'] },\n]\n`,\n )\n\n await writeFile(\n join(projectDir, 'lib/content-locale.ts'),\n `import { cookies } from 'next/headers'\nimport { hasLocale } from 'next-intl'\nimport { routing } from '@/i18n/routing'\n\n/**\n * Get the content editing locale from the cookie, falling back to the interface locale.\n */\nexport async function getContentLocale(interfaceLocale: string): Promise<string> {\n const jar = await cookies()\n const raw = jar.get('content-locale')?.value\n if (raw && hasLocale(routing.locales, raw)) return raw\n return interfaceLocale\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'lib/with-admin-context.ts'),\n `import { getContentConfig } from '@murumets-ee/content/plugin'\nimport type { RequestContext } from '@murumets-ee/core'\nimport { runWithContextAsync } from '@murumets-ee/core'\nimport { getToolkitApp } from './app'\nimport { getContentLocale } from './content-locale'\n\nexport async function withAdminContext<T>(\n interfaceLocale: string,\n fn: (ctx: { locale: string; defaultLocale: string }) => Promise<T>,\n): Promise<T> {\n const app = await getToolkitApp()\n const contentLocale = await getContentLocale(interfaceLocale)\n const { defaultLocale } = getContentConfig()\n\n const context: RequestContext = {\n locale: contentLocale,\n defaultLocale,\n app,\n }\n\n return runWithContextAsync(context, () => fn({ locale: contentLocale, defaultLocale }))\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'lib/load-roles.ts'),\n `import { buildInitialRoleDefinitions } from '@murumets-ee/auth'\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport { createSettingsClient } from '@murumets-ee/settings'\nimport { permissionSettings } from '@/settings/permissions'\n\nexport async function loadRoles(\n app: ToolkitApp,\n): Promise<Record<string, Record<string, string[]>>> {\n const client = createSettingsClient(permissionSettings, { app })\n const saved = await client.get('roles')\n\n if (saved) return saved\n\n // First run — seed built-in roles with zero permissions\n const initial = buildInitialRoleDefinitions()\n await client.set('roles', initial)\n return initial\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'settings/permissions.ts'),\n `import { defineSettings, setting } from '@murumets-ee/settings'\n\nexport const permissionSettings = defineSettings({\n namespace: 'permissions',\n scope: 'global',\n label: 'Permissions',\n schema: {\n roles: setting.json<Record<string, Record<string, string[]>>>(),\n },\n})\n`,\n )\n\n await writeFile(\n join(projectDir, 'settings/site.ts'),\n `import { defineSettings, setting } from '@murumets-ee/settings'\n\nexport const siteSettings = defineSettings({\n namespace: 'site',\n scope: 'global',\n label: 'Site Settings',\n schema: {\n siteName: setting.text({ default: 'My Site', label: 'Site Name', translatable: true }),\n siteDescription: setting.text({ label: 'Site Description', translatable: true }),\n maintenanceMode: setting.boolean({ default: false, label: 'Maintenance Mode' }),\n postsPerPage: setting.number({\n default: 10,\n min: 1,\n max: 100,\n integer: true,\n label: 'Posts Per Page',\n }),\n },\n})\n`,\n )\n\n // =========================================================================\n // Admin shell layout\n // =========================================================================\n\n await writeFile(\n join(projectDir, 'app/admin-layout.tsx'),\n `'use client'\n\nimport type { LinkComponent, SidebarNavGroup } from '@murumets-ee/admin-ui'\nimport { AdminShell } from '@murumets-ee/admin-ui'\nimport type { AdminNavGroup } from '@murumets-ee/admin-ui/server'\nimport {\n Activity,\n FileText,\n FolderTree,\n Home,\n Image,\n ImageDown,\n Lock,\n PenTool,\n ShieldCheck,\n Tags,\n Users,\n type LucideIcon,\n} from 'lucide-react'\nimport type { ReactNode } from 'react'\nimport { Link, usePathname } from '@/i18n/navigation'\n\n/** Map from icon name strings (from entity admin config) to Lucide components */\nconst ICON_MAP: Record<string, LucideIcon> = {\n 'file-text': FileText,\n 'folder-tree': FolderTree,\n tags: Tags,\n}\n\ninterface AdminLayoutProps {\n children: ReactNode\n defaultOpen?: boolean\n headerActions?: ReactNode\n sidebarFooter?: ReactNode\n entityNavGroups?: AdminNavGroup[]\n}\n\nexport function AdminLayout({\n children,\n defaultOpen,\n headerActions,\n sidebarFooter,\n entityNavGroups,\n}: AdminLayoutProps) {\n const pathname = usePathname()\n\n const dynamicGroups: SidebarNavGroup[] = (entityNavGroups ?? []).map((group) => ({\n label: group.label,\n items: group.items.map((item) => ({\n label: item.label,\n href: item.href,\n icon: item.iconName ? ICON_MAP[item.iconName] : undefined,\n })),\n }))\n\n const staticBefore: SidebarNavGroup = {\n items: [\n { label: 'Home', href: '/', icon: Home },\n { label: 'Admin', href: '/admin', icon: PenTool },\n ],\n }\n\n const systemGroup: SidebarNavGroup = {\n label: 'System',\n items: [\n { label: 'Users', href: '/admin/users', icon: Users },\n { label: 'Roles', href: '/admin/roles', icon: ShieldCheck },\n { label: 'Permissions', href: '/admin/permissions', icon: Lock },\n { label: 'Media', href: '/admin/media', icon: Image },\n { label: 'Image Styles', href: '/admin/media/image-styles', icon: ImageDown },\n { label: 'Activity', href: '/admin/activity', icon: Activity },\n ],\n }\n\n const navGroups: SidebarNavGroup[] = [\n staticBefore,\n ...dynamicGroups,\n systemGroup,\n ]\n\n return (\n <AdminShell\n defaultOpen={defaultOpen}\n sidebar={{\n navGroups,\n pathname,\n Link: Link as unknown as LinkComponent,\n logo: (\n <div className=\"flex h-8 items-center gap-2 px-2\">\n <span className=\"font-semibold text-sm text-zinc-700 dark:text-zinc-300 truncate\">\n Admin\n </span>\n </div>\n ),\n footer: sidebarFooter,\n }}\n header={{\n actions: headerActions,\n }}\n >\n {children}\n </AdminShell>\n )\n}\n`,\n )\n\n // Shell layout (server component)\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/layout.tsx'),\n `import { buildAdminNav } from '@murumets-ee/admin-ui/server'\nimport { cookies, headers } from 'next/headers'\nimport { redirect } from 'next/navigation'\nimport { Suspense } from 'react'\nimport { allEntities } from '@/lib/admin-config'\nimport { auth } from '@/lib/auth'\nimport { AdminLayout } from '../../admin-layout'\nimport { ThemeToggle } from '../../theme-toggle'\n\nasync function DynamicShell({\n children,\n interfaceLocale,\n}: {\n children: React.ReactNode\n interfaceLocale: string\n}) {\n const h = await headers()\n const session = await auth.api.getSession({ headers: h })\n if (!session?.user) {\n redirect(\\`/\\${interfaceLocale}/auth/sign-in\\`)\n }\n\n const jar = await cookies()\n const sidebarOpen = jar.get('sidebar:state')?.value !== 'false'\n const entityNavGroups = buildAdminNav(allEntities, { basePath: '/admin' })\n\n return (\n <AdminLayout\n defaultOpen={sidebarOpen}\n entityNavGroups={entityNavGroups}\n sidebarFooter={\n <div className=\"flex justify-center gap-2\">\n <ThemeToggle />\n </div>\n }\n >\n {children}\n </AdminLayout>\n )\n}\n\nexport default async function ShellLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise<{ locale: string }>\n}) {\n const { locale } = await params\n\n return (\n <Suspense>\n <DynamicShell interfaceLocale={locale}>{children}</DynamicShell>\n </Suspense>\n )\n}\n`,\n )\n\n // Admin auth guard layout\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/layout.tsx'),\n `import { headers } from 'next/headers'\nimport { redirect } from 'next/navigation'\nimport { auth } from '@/lib/auth'\n\nconst ALLOWED_ROLES = new Set(['admin', 'editor'])\n\nexport default async function AdminGuardLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise<{ locale: string }>\n}) {\n const { locale } = await params\n const h = await headers()\n const session = await auth.api.getSession({ headers: h })\n\n if (!session?.user) {\n redirect(\\`/\\${locale}/auth/sign-in\\`)\n }\n\n const role = (session.user as Record<string, unknown>).role as string | undefined\n if (!role || !ALLOWED_ROLES.has(role)) {\n redirect(\\`/\\${locale}\\`)\n }\n\n return <>{children}</>\n}\n`,\n )\n\n // =========================================================================\n // Admin pages\n // =========================================================================\n\n // Dashboard\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/page.tsx'),\n `import { setRequestLocale } from 'next-intl/server'\n\nexport default async function AdminDashboard({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n return (\n <div className=\"p-6\">\n <h1 className=\"text-2xl font-bold mb-4\">Dashboard</h1>\n <p className=\"text-muted-foreground\">\n Welcome to the admin panel. Use the sidebar to manage your content.\n </p>\n </div>\n )\n}\n`,\n )\n\n // Generic entity list\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/[entity]/page.tsx'),\n `import { EntityList } from '@murumets-ee/admin-ui/entity-list'\nimport {\n entityNameToSlug,\n fetchEntityList,\n getEntityLabel,\n resolveEntityFromSlug,\n toEntityMeta,\n} from '@murumets-ee/admin-ui/server'\nimport { getContentConfig } from '@murumets-ee/content/plugin'\nimport { notFound } from 'next/navigation'\nimport { setRequestLocale } from 'next-intl/server'\nimport { allEntities, taxonomyVocabularies } from '@/lib/admin-config'\nimport { withAdminContext } from '@/lib/with-admin-context'\n\ninterface EntityListPageProps {\n params: Promise<{ locale: string; entity: string }>\n}\n\nexport default async function EntityListPage({ params }: EntityListPageProps) {\n const { locale, entity: entitySlug } = await params\n setRequestLocale(locale)\n\n const entity = resolveEntityFromSlug(entitySlug, allEntities)\n if (!entity) notFound()\n\n const admin = entity.admin\n const meta = toEntityMeta(entity)\n const urlSlug = entityNameToSlug(entity.name)\n const hasTranslatable = Object.values(entity.allFields).some((f) => f.translatable)\n const isPublishable = entity.behaviors?.some((b) => b.name === 'publishable')\n const isTaxonomy = entity.name in taxonomyVocabularies\n\n return withAdminContext(locale, async ({ locale: contentLocale, defaultLocale }) => {\n let locales: Array<{ code: string; label: string }> | undefined\n if (hasTranslatable) {\n try {\n const config = getContentConfig()\n locales = config.locales\n } catch {\n // content plugin not available\n }\n }\n\n const initialData = await fetchEntityList(entity, {\n sortField: admin?.defaultSort ?? 'createdAt',\n sortDirection: admin?.defaultSortDirection ?? 'desc',\n limit: admin?.pageSize ?? 20,\n locale: contentLocale,\n includeTranslationStatus: hasTranslatable,\n })\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">{getEntityLabel(entity)}</h1>\n {admin?.description && (\n <p className=\"text-sm text-muted-foreground\">{admin.description}</p>\n )}\n </div>\n <EntityList\n entity={meta}\n allowDelete\n allowStatusToggle={isPublishable}\n {...(isTaxonomy ? { apiBasePath: '/api/admin/taxonomy', entityPath: entity.name } : {})}\n defaultSort={admin?.defaultSort ?? 'createdAt'}\n defaultSortDirection={admin?.defaultSortDirection ?? 'desc'}\n hiddenColumns={admin?.hiddenColumns}\n pageSize={admin?.pageSize}\n locale={contentLocale}\n locales={locales}\n defaultLocale={defaultLocale}\n showTranslationStatus={hasTranslatable && !!locales}\n searchPlaceholder={\\`Search \\${getEntityLabel(entity).toLowerCase()}...\\`}\n editHref={\\`/\\${locale}/admin/\\${urlSlug}/:id\\`}\n createHref={admin?.disableCreate ? undefined : \\`/\\${locale}/admin/\\${urlSlug}/new\\`}\n initialData={initialData}\n />\n </div>\n )\n })\n}\n`,\n )\n\n // Generic entity edit\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/[entity]/[id]/page.tsx'),\n `import {\n entityNameToSlug,\n fetchReferenceOptions,\n getBlocksFieldName,\n getEntityLabelSingular,\n getReferenceFields,\n inferRootFields,\n resolveEntityFromSlug,\n toEntityMeta,\n} from '@murumets-ee/admin-ui/server'\nimport { buildPermissionChecker } from '@murumets-ee/auth'\nimport { ContentClient } from '@murumets-ee/content/client'\nimport { LockService } from '@murumets-ee/content/lock'\nimport { createAdminClient } from '@murumets-ee/core/clients'\nimport { getApp } from '@murumets-ee/core'\nimport { prepareBlockEditor } from '@murumets-ee/editor/server'\nimport { headers } from 'next/headers'\nimport { notFound } from 'next/navigation'\nimport { setRequestLocale } from 'next-intl/server'\nimport { GenericBlockEditor } from '@murumets-ee/admin-ui/content-editor'\nimport { GenericEntityForm } from '@murumets-ee/admin-ui/entity-form'\nimport { allEntities } from '@/lib/admin-config'\nimport { auth } from '@/lib/auth'\nimport { loadRoles } from '@/lib/load-roles'\nimport { withAdminContext } from '@/lib/with-admin-context'\n\ninterface EditEntityPageProps {\n params: Promise<{ locale: string; entity: string; id: string }>\n}\n\nexport default async function EditEntityPage({ params }: EditEntityPageProps) {\n const { locale, entity: entitySlug, id } = await params\n setRequestLocale(locale)\n\n const entity = resolveEntityFromSlug(entitySlug, allEntities)\n if (!entity) notFound()\n\n const blocksField = getBlocksFieldName(entity)\n const urlSlug = entityNameToSlug(entity.name)\n\n return withAdminContext(locale, async ({ locale: contentLocale, defaultLocale }) => {\n const client = createAdminClient(entity)\n const isDefaultLocale = contentLocale === defaultLocale\n const isNonDefaultLocale = !isDefaultLocale\n\n const isVersionable = entity.behaviors?.some((b) => b.name === 'versionable') ?? false\n const isPublishable = entity.behaviors?.some((b) => b.name === 'publishable') ?? false\n\n let draft: { data: Record<string, unknown>; createdBy: string; createdByName: string | null; updatedAt: Date | string } | undefined\n let canPublish = false\n let lockResult: { acquired: boolean; lock?: { lockedBy: string; lockedByName: string | null; lockedAt: Date | string; expiresAt: Date | string } | null } | undefined\n const enableLocking = isPublishable\n\n if (isPublishable) {\n try {\n const app = getApp()\n const contentClient = new ContentClient({ entity, db: app.db.readWrite })\n const lockLocale = isDefaultLocale ? '_' : contentLocale\n\n const draftEntry = await contentClient.getDraft(id, lockLocale)\n if (draftEntry) {\n draft = {\n data: draftEntry.data,\n createdBy: draftEntry.createdBy,\n createdByName: draftEntry.createdByName,\n updatedAt: draftEntry.updatedAt,\n }\n }\n\n const session = await auth.api.getSession({ headers: await headers() })\n if (session?.user) {\n const role = (session.user as Record<string, unknown>).role as string | undefined\n if (role) {\n const roles = await loadRoles(app)\n const checker = buildPermissionChecker(roles)\n canPublish = checker(role, entity.name, 'publish')\n }\n\n const lockService = new LockService({ db: app.db.readWrite })\n const result = await lockService.acquireLock(\n entity.name, id, lockLocale,\n { id: session.user.id, name: session.user.name ?? undefined },\n )\n if (result.acquired) {\n lockResult = { acquired: true }\n } else {\n lockResult = { acquired: false, lock: result.lock }\n }\n }\n } catch {\n // Draft/permission/lock errors — proceed without\n }\n }\n\n // Block editor entity\n if (blocksField) {\n const record = isDefaultLocale\n ? await client.findById(id, { defaultLocale })\n : await client.findById(id, { locale: contentLocale, defaultLocale })\n if (!record) notFound()\n\n const rootFields = inferRootFields(entity, blocksField)\n const { blocks, initialData, mediaUrls, rootData, rootFieldDefs } =\n await prepareBlockEditor(\n entity as Parameters<typeof prepareBlockEditor>[0],\n blocksField,\n record,\n { rootFields },\n )\n\n return (\n <div className=\"flex h-[calc(100vh-3.5rem)] flex-col\">\n <div className=\"flex items-center gap-3 border-b px-6 py-3\">\n <h1 className=\"text-lg font-semibold\">\n {(record.title as string) ?? \\`Untitled \\${getEntityLabelSingular(entity)}\\`}\n </h1>\n <span className=\"rounded bg-muted px-2 py-0.5 text-xs text-muted-foreground uppercase\">\n {contentLocale}\n </span>\n </div>\n <div className=\"flex-1 overflow-hidden\">\n <GenericBlockEditor\n key={contentLocale}\n entityId={id}\n entityName={entity.name}\n blocksField={blocksField}\n blocks={blocks}\n initialData={initialData}\n mediaUrls={mediaUrls}\n rootData={rootData}\n rootFieldDefs={rootFieldDefs}\n saveLabel=\"Save\"\n locale={contentLocale}\n defaultLocale={defaultLocale}\n versionable={isVersionable}\n isPublishable={isPublishable}\n draft={draft}\n canPublish={canPublish}\n enableLocking={enableLocking}\n lockResult={lockResult}\n />\n </div>\n </div>\n )\n }\n\n // Form entity\n const [record, defaultLocaleData] = await Promise.all([\n client.findById(id, { locale: contentLocale }),\n isNonDefaultLocale ? client.findById(id) : Promise.resolve(null),\n ])\n if (!record) notFound()\n\n const refFields = getReferenceFields(entity)\n const referenceOptions: Record<string, Array<{ id: string; label: string }>> = {}\n\n if (refFields.length > 0) {\n const optionPromises = refFields.map(async ({ fieldName, entityName }) => {\n const refEntity = allEntities.find((e) => e.name === entityName)\n if (!refEntity) return { fieldName, options: [] }\n const options = await fetchReferenceOptions(refEntity, { locale: contentLocale })\n return { fieldName, options }\n })\n const results = await Promise.all(optionPromises)\n for (const { fieldName, options } of results) {\n referenceOptions[fieldName] = options\n }\n }\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Edit {getEntityLabelSingular(entity)}</h1>\n </div>\n <GenericEntityForm\n entity={toEntityMeta(entity)}\n id={id}\n locale={contentLocale}\n defaultLocale={defaultLocale}\n defaultLocaleData={\n isNonDefaultLocale && defaultLocaleData\n ? (defaultLocaleData as Record<string, unknown>)\n : undefined\n }\n initialData={record as Record<string, unknown>}\n referenceOptions={referenceOptions}\n listUrl={\\`/\\${locale}/admin/\\${urlSlug}\\`}\n versionable={isVersionable}\n draft={draft}\n canPublish={canPublish}\n enableLocking={enableLocking}\n lockResult={lockResult}\n />\n </div>\n )\n })\n}\n`,\n )\n\n // Generic entity create\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/[entity]/new/page.tsx'),\n `import {\n entityNameToSlug,\n fetchReferenceOptions,\n getBlocksFieldName,\n getEntityLabelSingular,\n getReferenceFields,\n resolveEntityFromSlug,\n toEntityMeta,\n} from '@murumets-ee/admin-ui/server'\nimport { createAdminClient } from '@murumets-ee/core/clients'\nimport { notFound, redirect } from 'next/navigation'\nimport { setRequestLocale } from 'next-intl/server'\nimport { GenericEntityForm } from '@murumets-ee/admin-ui/entity-form'\nimport { allEntities } from '@/lib/admin-config'\nimport { withAdminContext } from '@/lib/with-admin-context'\n\ninterface NewEntityPageProps {\n params: Promise<{ locale: string; entity: string }>\n}\n\nexport default async function NewEntityPage({ params }: NewEntityPageProps) {\n const { locale, entity: entitySlug } = await params\n setRequestLocale(locale)\n\n const entity = resolveEntityFromSlug(entitySlug, allEntities)\n if (!entity) notFound()\n if (entity.admin?.disableCreate) notFound()\n\n const blocksField = getBlocksFieldName(entity)\n const urlSlug = entityNameToSlug(entity.name)\n\n return withAdminContext(locale, async ({ locale: contentLocale }) => {\n // Block editor entities: create a draft and redirect to the edit page\n if (blocksField) {\n const client = createAdminClient(entity)\n const draft = await client.create({ title: 'Untitled' })\n const newId = (draft as Record<string, unknown>).id as string\n redirect(\\`/\\${locale}/admin/\\${urlSlug}/\\${newId}\\`)\n }\n\n // Form entities: render the create form\n const refFields = getReferenceFields(entity)\n const referenceOptions: Record<string, Array<{ id: string; label: string }>> = {}\n\n if (refFields.length > 0) {\n const optionPromises = refFields.map(async ({ fieldName, entityName }) => {\n const refEntity = allEntities.find((e) => e.name === entityName)\n if (!refEntity) return { fieldName, options: [] }\n const options = await fetchReferenceOptions(refEntity, { locale: contentLocale })\n return { fieldName, options }\n })\n const results = await Promise.all(optionPromises)\n for (const { fieldName, options } of results) {\n referenceOptions[fieldName] = options\n }\n }\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">New {getEntityLabelSingular(entity)}</h1>\n </div>\n <GenericEntityForm\n entity={toEntityMeta(entity)}\n locale={contentLocale}\n referenceOptions={referenceOptions}\n listUrl={\\`/\\${locale}/admin/\\${urlSlug}\\`}\n />\n </div>\n )\n })\n}\n`,\n )\n\n // =========================================================================\n // Media pages\n // =========================================================================\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/media/page.tsx'),\n `import { EntityList } from '@murumets-ee/admin-ui/entity-list'\nimport { fetchEntityList, toEntityMeta } from '@murumets-ee/admin-ui/server'\nimport { Media } from '@murumets-ee/media'\nimport { getMediaClient } from '@murumets-ee/media/client'\nimport { setRequestLocale } from 'next-intl/server'\n\nexport default async function MediaListPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n const initialData = await fetchEntityList(Media, { sortField: 'createdAt' })\n\n const mediaClient = await getMediaClient()\n const ids = initialData.items.map((i) => i.id as string)\n const thumbMap = await mediaClient.getVariantUrls(ids, 'thumbnail')\n for (const item of initialData.items) {\n item.thumbnailUrl = thumbMap.get(item.id as string) ?? ''\n }\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Media</h1>\n <p className=\"text-sm text-muted-foreground\">\n Manage uploaded media files.\n </p>\n </div>\n <EntityList\n entity={toEntityMeta(Media)}\n entityPath=\"media\"\n image={{ field: 'thumbnailUrl', size: 'sm', shape: 'rounded' }}\n allowDelete\n defaultSort=\"createdAt\"\n searchPlaceholder=\"Search media...\"\n editHref={\\`/\\${locale}/admin/media/:id\\`}\n initialData={initialData}\n />\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/media/[id]/page.tsx'),\n `import { toEntityMeta } from '@murumets-ee/admin-ui/server'\nimport { getCurrentApp } from '@murumets-ee/core'\nimport { createAdminClient } from '@murumets-ee/core/clients'\nimport { Media } from '@murumets-ee/media'\nimport { findMediaUsages } from '@murumets-ee/media/usage'\nimport { notFound } from 'next/navigation'\nimport { setRequestLocale } from 'next-intl/server'\nimport { withAdminContext } from '@/lib/with-admin-context'\nimport { MediaForm } from './media-form'\nimport { MediaUsagePanel } from './media-usage-panel'\n\ninterface MediaEditPageProps {\n params: Promise<{ locale: string; id: string }>\n}\n\nexport default async function MediaEditPage({ params }: MediaEditPageProps) {\n const { locale, id } = await params\n setRequestLocale(locale)\n\n return withAdminContext(locale, async ({ locale: contentLocale }) => {\n const app = getCurrentApp()!\n const client = createAdminClient(Media)\n const [mediaItem, usages] = await Promise.all([\n client.findById(id, { locale: contentLocale }),\n findMediaUsages(id, app.db.readWrite),\n ])\n if (!mediaItem) notFound()\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Edit Media</h1>\n </div>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <MediaForm\n entity={toEntityMeta(Media)}\n id={id}\n locale={contentLocale}\n initialData={mediaItem as Record<string, unknown>}\n />\n </div>\n <div>\n <MediaUsagePanel usages={usages} locale={locale} />\n </div>\n </div>\n </div>\n )\n })\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/media/[id]/media-form.tsx'),\n `'use client'\n\nimport { EntityForm } from '@murumets-ee/admin-ui/entity-form'\nimport type { EntityMeta } from '@murumets-ee/admin-ui/entity-list'\nimport { useRouter } from 'next/navigation'\n\ninterface MediaFormProps {\n entity: EntityMeta\n id: string\n locale?: string\n initialData?: Record<string, unknown>\n}\n\nexport function MediaForm({ entity, id, locale, initialData }: MediaFormProps) {\n const router = useRouter()\n\n return (\n <EntityForm\n entity={entity}\n id={id}\n locale={locale}\n initialData={initialData}\n entityPath=\"media\"\n layout=\"two-column\"\n fields={['title', 'alt', 'description', 'filename', 'mimeType', 'size', 'mediaType']}\n fieldOverrides={{\n filename: { readOnly: true, description: 'Original upload filename (read-only)' },\n mimeType: { readOnly: true, label: 'MIME Type' },\n size: { readOnly: true, description: 'File size in bytes' },\n mediaType: { readOnly: true, label: 'Media Type' },\n alt: { label: 'Alt Text', description: 'Alternative text for accessibility' },\n }}\n onSuccess={() => router.push(\\`/\\${locale}/admin/media\\`)}\n onCancel={() => router.back()}\n />\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/media/[id]/media-usage-panel.tsx'),\n `import type { MediaUsage } from '@murumets-ee/media/usage'\n\ninterface MediaUsagePanelProps {\n usages: MediaUsage[]\n locale: string\n}\n\nexport function MediaUsagePanel({ usages, locale }: MediaUsagePanelProps) {\n return (\n <div className=\"rounded-lg border bg-card p-4\">\n <h3 className=\"mb-3 text-sm font-semibold\">Used by</h3>\n\n {usages.length === 0 && (\n <p className=\"text-sm text-muted-foreground\">\n This media item is not referenced by any entity.\n </p>\n )}\n\n {usages.length > 0 && (\n <ul className=\"space-y-2\">\n {usages.map((usage) => {\n const entitySlug = \\`\\${usage.entityName}s\\`\n const href = \\`/\\${locale}/admin/\\${entitySlug}/\\${usage.entityId}\\`\n\n return (\n <li key={\\`\\${usage.entityName}-\\${usage.entityId}-\\${usage.fieldName}\\`}>\n <a\n href={href}\n className=\"block rounded-md border px-3 py-2 text-sm hover:bg-muted transition-colors\"\n >\n <span className=\"font-medium capitalize\">{usage.entityName}</span>\n <span className=\"mx-1.5 text-muted-foreground\">&middot;</span>\n <span className=\"text-muted-foreground\">{usage.fieldName}</span>\n {usage.context === 'block' && (\n <span className=\"ml-1.5 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground\">\n block\n </span>\n )}\n </a>\n </li>\n )\n })}\n </ul>\n )}\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/media/image-styles/page.tsx'),\n `import { imageStylesSettings } from '@murumets-ee/media'\nimport { ImageStylesManager } from '@murumets-ee/media/image-styles'\nimport { createSettingsClient } from '@murumets-ee/settings'\nimport { setRequestLocale } from 'next-intl/server'\nimport { getToolkitApp } from '@/lib/app'\n\nasync function loadImageStyles() {\n const app = await getToolkitApp()\n const client = createSettingsClient(imageStylesSettings, { app })\n const styles = await client.get('imageStyles')\n return styles ?? {}\n}\n\nexport default async function ImageStylesPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n const initialStyles = await loadImageStyles()\n\n return (\n <div className=\"p-6 max-w-4xl\">\n <ImageStylesManager initialStyles={initialStyles} />\n </div>\n )\n}\n`,\n )\n\n // =========================================================================\n // Users, roles, permissions, activity\n // =========================================================================\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/users/page.tsx'),\n `import type { UsersInitialData } from '@murumets-ee/admin-ui/users'\nimport { getAuth } from '@murumets-ee/auth'\nimport { headers } from 'next/headers'\nimport { setRequestLocale } from 'next-intl/server'\nimport { getToolkitApp } from '@/lib/app'\nimport { UsersPage } from './users-page'\n\nexport default async function UsersAdminPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n await getToolkitApp()\n const auth = getAuth()\n\n const result = await auth.api.listUsers({\n headers: await headers(),\n query: { limit: 20, sortBy: 'createdAt', sortDirection: 'desc' },\n })\n\n const initialData: UsersInitialData = {\n users: result.users.map((u) => ({\n id: u.id,\n name: u.name,\n email: u.email,\n emailVerified: u.emailVerified,\n image: u.image ?? null,\n createdAt: u.createdAt instanceof Date ? u.createdAt.toISOString() : String(u.createdAt),\n updatedAt: u.updatedAt instanceof Date ? u.updatedAt.toISOString() : String(u.updatedAt),\n role: u.role ?? null,\n banned: u.banned ?? null,\n banReason: u.banReason ?? null,\n banExpires:\n u.banExpires instanceof Date\n ? u.banExpires.toISOString()\n : u.banExpires\n ? String(u.banExpires)\n : null,\n })),\n total: result.total,\n }\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Users</h1>\n <p className=\"text-sm text-muted-foreground\">Manage user accounts, roles, and access.</p>\n </div>\n <UsersPage initialData={initialData} />\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/users/users-page.tsx'),\n `'use client'\n\nimport type { UsersInitialData } from '@murumets-ee/admin-ui/users'\nimport { UsersManagement } from '@murumets-ee/admin-ui/users'\nimport { createUsersApi } from '@murumets-ee/auth/client'\nimport { authClient } from '@/lib/auth-client'\n\nconst usersApi = createUsersApi(authClient)\n\nexport function UsersPage({ initialData }: { initialData: UsersInitialData }) {\n const session = authClient.useSession()\n\n return (\n <UsersManagement\n api={usersApi}\n currentUserId={session.data?.user?.id}\n roles={['admin', 'editor', 'viewer']}\n initialData={initialData}\n />\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/permissions/page.tsx'),\n `import { PermissionsEditor } from '@murumets-ee/admin-ui/permissions'\nimport { BUILT_IN_ROLES, buildResourceCatalog } from '@murumets-ee/auth'\nimport { setRequestLocale } from 'next-intl/server'\nimport { allEntities, pluginResources } from '@/lib/admin-config'\nimport { getToolkitApp } from '@/lib/app'\nimport { loadRoles } from '@/lib/load-roles'\n\nexport default async function PermissionsPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n const app = await getToolkitApp()\n const savedRoles = await loadRoles(app)\n const statements = buildResourceCatalog(allEntities, pluginResources)\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Permissions</h1>\n <p className=\"text-sm text-muted-foreground\">\n Configure what each role can do. Admin always has full access.\n </p>\n </div>\n <PermissionsEditor\n statements={statements}\n roles={Object.keys(savedRoles)}\n builtInRoles={[...BUILT_IN_ROLES]}\n initialPermissions={savedRoles}\n />\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/roles/page.tsx'),\n `import { RolesEditor } from '@murumets-ee/admin-ui/permissions'\nimport { BUILT_IN_ROLES } from '@murumets-ee/auth'\nimport { setRequestLocale } from 'next-intl/server'\nimport { getToolkitApp } from '@/lib/app'\nimport { loadRoles } from '@/lib/load-roles'\n\nexport default async function RolesPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n const app = await getToolkitApp()\n const savedRoles = await loadRoles(app)\n\n const roles = [\n { name: 'admin', builtIn: true, permissionCount: 0 },\n ...Object.entries(savedRoles).map(([name, perms]) => ({\n name,\n builtIn: (BUILT_IN_ROLES as readonly string[]).includes(name),\n permissionCount: Object.values(perms).flat().length,\n })),\n ]\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Roles</h1>\n <p className=\"text-sm text-muted-foreground\">\n Manage user roles. Built-in roles cannot be deleted.\n </p>\n </div>\n <RolesEditor roles={roles} permissionsHref={\\`/\\${locale}/admin/permissions\\`} />\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(projectDir, 'app/[locale]/(shell)/admin/activity/page.tsx'),\n `import { AuditLog } from '@murumets-ee/admin-ui/audit-log'\nimport { fetchAuditLogData } from '@murumets-ee/admin-ui/server'\nimport { setRequestLocale } from 'next-intl/server'\nimport { withAdminContext } from '@/lib/with-admin-context'\n\nexport default async function ActivityPage({ params }: { params: Promise<{ locale: string }> }) {\n const { locale } = await params\n setRequestLocale(locale)\n\n return withAdminContext(locale, async () => {\n const initialData = await fetchAuditLogData()\n\n return (\n <div className=\"p-6\">\n <div className=\"mb-6\">\n <h1 className=\"text-2xl font-bold\">Activity Log</h1>\n <p className=\"text-sm text-muted-foreground\">\n Track all content changes across your CMS.\n </p>\n </div>\n <AuditLog\n initialData={initialData}\n editHrefPattern={\\`/\\${locale}/admin/:entityType/:entityId\\`}\n />\n </div>\n )\n })\n}\n`,\n )\n\n // =========================================================================\n // Admin API route handler\n // =========================================================================\n\n await writeFile(\n join(projectDir, 'app/api/admin/[...path]/route.ts'),\n `import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'\nimport { buildPermissionChecker, buildResourceCatalog } from '@murumets-ee/auth'\nimport { permissionRoutes } from '@murumets-ee/auth/admin'\nimport { ContentClient } from '@murumets-ee/content/client'\nimport { LockService } from '@murumets-ee/content/lock'\nimport type { PermissionChecker } from '@murumets-ee/core'\nimport { getApp } from '@murumets-ee/core'\nimport {\n AuditLogClient,\n createAuditDbWriter,\n createAuditLogger,\n createLogger,\n} from '@murumets-ee/logging'\nimport { logRoutes } from '@murumets-ee/logging/admin'\nimport { mediaRoutes } from '@murumets-ee/media/admin'\nimport { createSettingsClient } from '@murumets-ee/settings'\nimport { settingsRoutes } from '@murumets-ee/settings/admin'\nimport { storageRoutes } from '@murumets-ee/storage/admin'\nimport { taxonomyRoutes } from '@murumets-ee/taxonomy/admin'\nimport {\n allEntities,\n crudEntities,\n pluginResources,\n taxonomyVocabularies,\n} from '@/lib/admin-config'\nimport { getToolkitApp } from '@/lib/app'\nimport { auth } from '@/lib/auth'\nimport { loadRoles } from '@/lib/load-roles'\nimport { permissionSettings } from '@/settings/permissions'\nimport { siteSettings } from '@/settings/site'\n\nawait getToolkitApp()\n\nlet _auditLogger: ReturnType<typeof createAuditLogger> | null = null\nlet _checker: PermissionChecker | null = null\n\nconst routes = [\n mediaRoutes(),\n storageRoutes(),\n settingsRoutes(siteSettings),\n taxonomyRoutes(taxonomyVocabularies),\n logRoutes(() => new AuditLogClient(getApp().db.readWrite)),\n permissionRoutes({\n getStatements: () => buildResourceCatalog(allEntities, pluginResources),\n loadRoles: async () => loadRoles(getApp()),\n saveRoles: async (roles) => {\n const app = getApp()\n const client = createSettingsClient(permissionSettings, { app })\n await client.set('roles', roles)\n },\n onSave: () => {\n _checker = null\n },\n }),\n]\n\nconst handler = createAdminApiHandler({\n authenticate: async (req) => {\n const session = await auth.api.getSession({ headers: req.headers })\n if (!session?.user) return null\n const role = (session.user as Record<string, unknown>).role as string | undefined\n return { id: session.user.id, role, name: session.user.name, email: session.user.email }\n },\n defaultLocale: 'en',\n entities: crudEntities,\n routes,\n loadPermissions: async () => {\n if (_checker) return _checker\n const roles = await loadRoles(getApp())\n _checker = buildPermissionChecker(roles)\n return _checker\n },\n auditLogger: {\n log: async (entry) => {\n if (!_auditLogger) {\n const app = getApp()\n _auditLogger = createAuditLogger({\n logger: createLogger({ name: 'audit' }),\n dbWriter: createAuditDbWriter(app.db.readWrite),\n })\n }\n return _auditLogger.log(entry)\n },\n },\n contentClientFactory: (entity) =>\n new ContentClient({ entity, db: getApp().db.readWrite }),\n lockServiceFactory: () =>\n new LockService({ db: getApp().db.readWrite }),\n})\n\nexport const { GET, POST, PATCH, DELETE } = handler\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateAuth(projectDir: string, _options: ProjectOptions): Promise<void> {\n // lib/auth.ts — lazy init (no top-level await, safe during next build)\n await writeFile(\n join(projectDir, 'lib/auth.ts'),\n `import { getToolkitApp } from './app'\nimport { getAuth } from '@murumets-ee/auth'\n\nexport async function getAuthInstance() {\n await getToolkitApp()\n return getAuth()\n}\n`,\n )\n\n // lib/auth-client.ts\n await writeFile(\n join(projectDir, 'lib/auth-client.ts'),\n `import { createClient } from '@murumets-ee/auth/client'\n\nexport const authClient = createClient()\n`,\n )\n\n // app/api/auth/[...all]/route.ts — lazy init, no DB connection at build time\n await writeFile(\n join(projectDir, 'app/api/auth/[...all]/route.ts'),\n `import { toNextJsHandler } from 'better-auth/next-js'\nimport { getAuthInstance } from '../../../../lib/auth'\n\nlet _handler: ReturnType<typeof toNextJsHandler> | null = null\n\nasync function handler() {\n if (!_handler) {\n const auth = await getAuthInstance()\n _handler = toNextJsHandler(auth)\n }\n return _handler\n}\n\nexport async function GET(req: Request) {\n const h = await handler()\n return h.GET(req)\n}\n\nexport async function POST(req: Request) {\n const h = await handler()\n return h.POST(req)\n}\n`,\n )\n\n // app/[locale]/auth/layout.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/layout.tsx'),\n `import { AuthProviders } from './providers'\nimport type { ReactNode } from 'react'\n\nexport default function AuthLayout({ children }: { children: ReactNode }) {\n return (\n <AuthProviders>\n <div className=\"flex min-h-[calc(100vh-3rem)] items-center justify-center px-4\">\n {children}\n </div>\n </AuthProviders>\n )\n}\n`,\n )\n\n // app/[locale]/auth/providers.tsx — adapted: uses next/link instead of @/i18n/navigation\n await writeFile(\n join(projectDir, 'app/[locale]/auth/providers.tsx'),\n `'use client'\n\nimport { AuthUIProvider } from '@murumets-ee/auth-ui'\nimport { authClient } from '@/lib/auth-client'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useLocale } from 'next-intl'\nimport type { ReactNode } from 'react'\n\nexport function AuthProviders({ children }: { children: ReactNode }) {\n const router = useRouter()\n const locale = useLocale()\n\n return (\n <AuthUIProvider\n authClient={authClient as never}\n basePath=\"/auth\"\n redirectTo=\"/\"\n Link={Link}\n navigate={(url) => router.push(url)}\n resetPasswordUrl={\\`/\\${locale}/auth/reset-password\\`}\n >\n {children}\n </AuthUIProvider>\n )\n}\n`,\n )\n\n // app/[locale]/auth/sign-in/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/sign-in/page.tsx'),\n `import { SignInForm } from '@murumets-ee/auth-ui'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n`,\n )\n\n // app/[locale]/auth/sign-up/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/sign-up/page.tsx'),\n `import { SignUpForm } from '@murumets-ee/auth-ui'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n`,\n )\n\n // app/[locale]/auth/forgot-password/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/forgot-password/page.tsx'),\n `import { ForgotPasswordForm } from '@murumets-ee/auth-ui'\n\nexport default function ForgotPasswordPage() {\n return <ForgotPasswordForm />\n}\n`,\n )\n\n // app/[locale]/auth/reset-password/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/reset-password/page.tsx'),\n `import { Suspense } from 'react'\nimport { ResetPasswordContent } from './content'\n\nexport default function ResetPasswordPage() {\n return (\n <Suspense>\n <ResetPasswordContent />\n </Suspense>\n )\n}\n`,\n )\n\n // app/[locale]/auth/reset-password/content.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/auth/reset-password/content.tsx'),\n `'use client'\n\nimport { useSearchParams } from 'next/navigation'\nimport { ResetPasswordForm } from '@murumets-ee/auth-ui'\n\nexport function ResetPasswordContent() {\n const searchParams = useSearchParams()\n const token = searchParams.get('token')\n\n if (!token) {\n return (\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-bold text-zinc-900 dark:text-zinc-50\">\n Invalid or expired link\n </h1>\n <p className=\"mt-2 text-zinc-500\">\n Please request a new password reset.\n </p>\n </div>\n )\n }\n\n return <ResetPasswordForm token={token} />\n}\n`,\n )\n\n // app/[locale]/setup/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/setup/page.tsx'),\n `import { redirect } from 'next/navigation'\nimport { sql } from 'drizzle-orm'\nimport { getToolkitApp } from '@/lib/app'\nimport { SetupForm } from './form'\n\nexport default async function SetupPage() {\n const app = await getToolkitApp()\n const result = await app.db.readOnly.execute<{ count: string }>(\n sql\\`SELECT COUNT(*)::text as count FROM \"user\"\\`,\n )\n\n if (Number(result[0]?.count) > 0) {\n redirect('/')\n }\n\n return (\n <div className=\"max-w-md mx-auto p-8\">\n <h1 className=\"text-2xl font-bold mb-2\">Create Admin</h1>\n <p className=\"text-sm text-zinc-500 dark:text-zinc-400 mb-6\">\n No users found. Create the first admin account.\n </p>\n <SetupForm />\n </div>\n )\n}\n`,\n )\n\n // app/[locale]/setup/form.tsx — adapted: success link to /auth/sign-in\n await writeFile(\n join(projectDir, 'app/[locale]/setup/form.tsx'),\n `'use client'\n\nimport { useState } from 'react'\nimport { createFirstAdmin } from './actions'\n\nexport function SetupForm() {\n const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)\n const [loading, setLoading] = useState(false)\n\n async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setLoading(true)\n setResult(null)\n const res = await createFirstAdmin(new FormData(e.currentTarget))\n setResult(res)\n setLoading(false)\n }\n\n if (result?.ok) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-green-600 dark:text-green-400\">Admin created: {result.email}</p>\n <a href=\"/auth/sign-in\" className=\"text-blue-600 dark:text-blue-400 hover:underline\">\n Sign in &rarr;\n </a>\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n <label className=\"text-sm font-medium\">\n Name\n <input\n name=\"name\"\n required\n 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\"\n />\n </label>\n <label className=\"text-sm font-medium\">\n Email\n <input\n name=\"email\"\n type=\"email\"\n required\n 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\"\n />\n </label>\n <label className=\"text-sm font-medium\">\n Password (min 8 chars)\n <input\n name=\"password\"\n type=\"password\"\n required\n minLength={8}\n 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\"\n />\n </label>\n\n {result?.error && <p className=\"text-red-600 dark:text-red-400 text-sm\">{result.error}</p>}\n\n <button\n type=\"submit\"\n disabled={loading}\n className=\"px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors\"\n >\n {loading ? 'Creating...' : 'Create Admin'}\n </button>\n </form>\n )\n}\n`,\n )\n\n // app/[locale]/setup/actions.ts\n await writeFile(\n join(projectDir, 'app/[locale]/setup/actions.ts'),\n `'use server'\n\nimport { sql } from 'drizzle-orm'\nimport { getToolkitApp } from '@/lib/app'\nimport { getAuth } from '@murumets-ee/auth'\n\n/**\n * Create the first admin user. Locked down:\n * - Advisory lock prevents race conditions (two simultaneous requests)\n * - User count check inside the lock ensures only one admin can be created\n * - Once any user exists, this action permanently refuses\n */\nexport async function createFirstAdmin(formData: FormData) {\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n const name = formData.get('name') as string\n\n if (!email || !password || !name) {\n return { error: 'All fields are required' }\n }\n\n if (password.length < 8) {\n return { error: 'Password must be at least 8 characters' }\n }\n\n const app = await getToolkitApp()\n\n // Acquire advisory lock + check atomically in a transaction\n const canCreate = await app.db.readWrite.transaction(async (tx) => {\n // Advisory lock 1 = \"setup lock\". Blocks concurrent setup attempts.\n await tx.execute(sql\\`SELECT pg_advisory_xact_lock(1)\\`)\n\n const result = await tx.execute<{ count: string }>(\n sql\\`SELECT COUNT(*)::text as count FROM \"user\"\\`,\n )\n return Number(result[0]?.count) === 0\n })\n\n if (!canCreate) {\n return { error: 'Admin user already exists. Setup is complete.' }\n }\n\n const auth = getAuth()\n\n const adminUser = await auth.api.signUpEmail({\n body: { email, password, name },\n })\n\n await app.db.readWrite.execute(\n sql\\`UPDATE \"user\" SET role = 'admin' WHERE id = \\${adminUser.user.id}\\`,\n )\n\n return { ok: true, email: adminUser.user.email }\n}\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { appendToFile, writeFile } from '../utils'\n\nexport async function generateBase(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n\n // .env.example\n await writeFile(\n join(projectDir, '.env.example'),\n `DATABASE_URL=postgresql://${name}:${name}_dev_password@localhost:5432/${name}_dev\nBETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars\nBETTER_AUTH_URL=http://localhost:3000\nLOG_LEVEL=debug\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n# Queue worker — by default runs embedded in the web process.\n# Set to \"false\" in production and run \"pnpm worker\" as a separate process.\n# QUEUE_WORKER=false\nRESEND_API_KEY=\nRESEND_WEBHOOK_SECRET=\nMAIL_FROM=noreply@example.com\nCSAT_SECRET=\n`,\n )\n\n // .dockerignore\n await writeFile(\n join(projectDir, '.dockerignore'),\n `# Dependencies (installed inside Docker)\nnode_modules/\n.pnpm-store/\n\n# Build outputs (rebuilt inside Docker)\n.next/\nout/\ndist/\n\n# Git\n.git/\n.gitignore\n\n# Environment files (pass via docker run --env-file)\n.env\n.env.*\n!.env.example\n\n# Docker files\nDockerfile*\n.dockerignore\ndocker-compose*.yml\n\n# Tests\ncoverage/\n**/*.test.*\n**/*.spec.*\n**/vitest.config.*\ndocker-compose.test.yml\n\n# Development tooling\n.vscode/\n.idea/\n.turbo/\n.cache/\n*.tsbuildinfo\n\n# Documentation\n*.md\ndocs/\n\n# OS files\n.DS_Store\nThumbs.db\n`,\n )\n\n // Dockerfile — Next.js 16 standalone with pnpm\n // Based on https://github.com/vercel/next.js/tree/canary/examples/with-docker\n await writeFile(\n join(projectDir, 'Dockerfile'),\n `# --- Base image ---\n# Node 22 LTS (Debian slim — glibc required for sharp image optimization)\nFROM node:22-slim AS base\n\n# --- Stage 1: Install dependencies ---\nFROM base AS deps\nWORKDIR /app\nRUN corepack enable pnpm\nCOPY package.json pnpm-lock.yaml ./\nRUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \\\\\n pnpm install --frozen-lockfile\n\n# --- Stage 2: Build the application ---\nFROM base AS builder\nWORKDIR /app\nRUN corepack enable pnpm\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nENV NODE_ENV=production\nENV NEXT_TELEMETRY_DISABLED=1\n# Dummy env vars so toolkit.config.ts doesn't throw at build time (no DB connection needed)\nENV DATABASE_URL=postgresql://build:build@localhost:5432/build\nENV BETTER_AUTH_SECRET=build-secret-not-used-at-runtime\nRUN pnpm build\n\n# --- Stage 3: Production runner ---\nFROM base AS runner\nWORKDIR /app\nENV NODE_ENV=production\nENV PORT=3000\nENV HOSTNAME=0.0.0.0\nENV NEXT_TELEMETRY_DISABLED=1\n\n# Copy standalone output (includes server.js + traced node_modules)\nCOPY --from=builder --chown=node:node /app/.next/standalone ./\n# Copy static assets (JS/CSS chunks — excluded from standalone trace)\nCOPY --from=builder --chown=node:node /app/.next/static ./.next/static\n# Copy public assets (favicon, robots.txt, images)\nCOPY --from=builder --chown=node:node /app/public ./public\n# Copy migrations (applied at app startup by the toolkit migration runner)\nCOPY --from=builder --chown=node:node /app/migrations ./migrations\n\n# Run as non-root user\nUSER node\nEXPOSE 3000\nCMD [\"node\", \"server.js\"]\n`,\n )\n\n // docker-compose.yml — local development (Postgres only)\n await writeFile(\n join(projectDir, 'docker-compose.yml'),\n `services:\n postgres:\n image: postgres:17-alpine\n container_name: ${name}-postgres\n restart: unless-stopped\n environment:\n POSTGRES_USER: ${name}\n POSTGRES_PASSWORD: ${name}_dev_password\n POSTGRES_DB: ${name}_dev\n ports:\n - \"5432:5432\"\n volumes:\n - postgres_data:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${name} -d ${name}_dev\"]\n interval: 10s\n timeout: 5s\n retries: 5\n\nvolumes:\n postgres_data:\n name: ${name}_postgres_data\n`,\n )\n\n // docker-compose.prod.yml — production (app + Postgres)\n await writeFile(\n join(projectDir, 'docker-compose.prod.yml'),\n `services:\n app:\n build: .\n restart: unless-stopped\n env_file: .env\n depends_on:\n postgres:\n condition: service_healthy\n ports:\n - \"3000:3000\"\n networks:\n - internal\n\n postgres:\n image: postgres:17-alpine\n restart: unless-stopped\n environment:\n POSTGRES_USER: ${name}\n POSTGRES_PASSWORD: \\${DB_PASSWORD}\n POSTGRES_DB: ${name}\n volumes:\n - postgres_data:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${name} -d ${name}\"]\n interval: 10s\n timeout: 5s\n retries: 5\n networks:\n - internal\n\nvolumes:\n postgres_data:\n\nnetworks:\n internal:\n`,\n )\n\n // .gitignore augmentation\n await appendToFile(\n join(projectDir, '.gitignore'),\n `\n# Toolkit generated files\ngenerated/\n\n# Environment\n.env*\n!.env.example\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateEntities(\n projectDir: string,\n _options: ProjectOptions,\n): Promise<void> {\n await writeFile(\n join(projectDir, 'entities/index.ts'),\n `export { Article } from './article'\nexport { Category } from './category'\n`,\n )\n\n await writeFile(\n join(projectDir, 'entities/article.ts'),\n `import { behavior, defineEntity, field } from '@murumets-ee/entity'\n\nexport const Article = defineEntity({\n name: 'article',\n fields: {\n title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),\n slug: field.slug({ from: 'title', unique: true }),\n excerpt: field.text({ maxLength: 500, translatable: true }),\n body: field.richtext(),\n viewCount: field.number({ default: 0, integer: true }),\n featured: field.boolean({ default: false }),\n publishDate: field.date(),\n contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),\n category: field.reference({ entity: 'category', required: false }),\n tags: field.reference({ entity: 'category', cardinality: 'many' }),\n coverImage: field.media({ accept: ['image/*'] }),\n },\n behaviors: [\n behavior.publishable(),\n behavior.auditable(),\n behavior.sluggable('title'),\n behavior.revisionable(),\n ],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n`,\n )\n\n await writeFile(\n join(projectDir, 'entities/category.ts'),\n `import { defineEntity, field } from '@murumets-ee/entity'\n\nexport const Category = defineEntity({\n name: 'category',\n fields: {\n name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),\n slug: field.slug({ from: 'name', unique: true }),\n description: field.text({ maxLength: 500, translatable: true }),\n },\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateIntl(projectDir: string, _options: ProjectOptions): Promise<void> {\n // i18n/routing.ts\n await writeFile(\n join(projectDir, 'i18n/routing.ts'),\n `import { defineRouting } from 'next-intl/routing'\n\nexport const routing = defineRouting({\n locales: ['en'],\n defaultLocale: 'en',\n})\n`,\n )\n\n // i18n/request.ts\n await writeFile(\n join(projectDir, 'i18n/request.ts'),\n `import { getRequestConfig } from 'next-intl/server'\nimport { hasLocale } from 'next-intl'\nimport { routing } from './routing'\nimport { getAuthMessages } from '@murumets-ee/auth-ui/i18n'\n\nexport default getRequestConfig(async ({ requestLocale }) => {\n const requested = await requestLocale\n const locale = hasLocale(routing.locales, requested)\n ? requested\n : routing.defaultLocale\n\n const authMessages = await getAuthMessages(locale)\n\n return {\n locale,\n messages: {\n ...authMessages,\n },\n }\n})\n`,\n )\n}\n","export const TOOLKIT_VERSION = '0.1.0'\nexport const CREATE_NEXT_APP_VERSION = '16'\n\nexport const DEPS = {\n // toolkit packages\n myorgCore: '0.1.0',\n myorgDb: '0.1.0',\n myorgEntity: '0.1.0',\n myorgLogging: '0.1.0',\n myorgAuth: '0.1.0',\n myorgAuthUi: '0.1.0',\n myorgAdminUi: '0.1.0',\n myorgContent: '0.1.0',\n myorgContentApi: '0.1.0',\n myorgSettings: '0.1.0',\n myorgStorage: '0.1.0',\n myorgMedia: '0.1.0',\n myorgTaxonomy: '0.1.0',\n myorgQueue: '0.1.0',\n myorgMail: '0.1.0',\n myorgTicketing: '0.1.0',\n myorgTicketingUi: '0.1.0',\n myorgEditor: '0.1.0',\n myorgBlocks: '0.1.0',\n myorgTokens: '0.1.0',\n myorgUi: '0.1.0',\n myorgCli: '0.1.0',\n // third-party runtime\n betterAuth: '1.4.0',\n drizzleOrm: '0.45.1',\n nextIntl: '4.8.2',\n nextThemes: '0.4.6',\n lucideReact: '0.563.0',\n postgres: '3.4.5',\n reactHookForm: '7.71.1',\n hookformResolvers: '5.2.2',\n zod: '3.24.1',\n tanstackReactQuery: '5.60.5',\n tanstackReactTable: '8.21.3',\n // third-party dev\n drizzleKit: '0.31.10',\n tsx: '4.19.2',\n babelReactCompiler: '1.0.0',\n} as const\n","import { mkdir } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { deleteFiles, mergePackageJson, runCommand, writeFile } from '../utils'\nimport { CREATE_NEXT_APP_VERSION, DEPS } from '../versions'\n\nexport async function generateMulti(\n projectDir: string,\n options: ProjectOptions,\n onProgress?: (message: string) => void,\n): Promise<void> {\n await generateWorkspaceRoot(projectDir, options)\n onProgress?.('Workspace root created')\n\n onProgress?.('Creating admin app...')\n await createAndOverlayAdmin(projectDir, options)\n onProgress?.('Admin app created')\n\n onProgress?.('Creating web app...')\n await createAndOverlayWeb(projectDir, options)\n onProgress?.('Web app created')\n\n await generateSharedConfig(projectDir, options)\n onProgress?.('Shared config package created')\n}\n\n// ──────────────────────────────────────────────────────\n// Root workspace\n// ──────────────────────────────────────────────────────\n\nasync function generateWorkspaceRoot(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n\n await mkdir(projectDir, { recursive: true })\n\n // pnpm-workspace.yaml (includes build script approval for pnpm v10+)\n await writeFile(\n join(projectDir, 'pnpm-workspace.yaml'),\n `packages:\n - 'packages/*'\n - 'apps/*'\n\nignoredBuiltDependencies:\n - sharp\n - unrs-resolver\n`,\n )\n\n // root package.json\n await writeFile(\n join(projectDir, 'package.json'),\n `${JSON.stringify(\n {\n name: `@${name}/monorepo`,\n version: '0.0.0',\n private: true,\n type: 'module',\n scripts: {\n dev: 'turbo dev',\n build: 'turbo build',\n 'db:generate': 'tsx --env-file=.env packages/config/scripts/generate-schema.ts',\n 'db:migrate:generate': 'drizzle-kit generate --config packages/config/drizzle.config.ts',\n 'db:migrate': 'tsx --env-file=.env packages/config/scripts/migrate.ts',\n 'db:reset': 'tsx --env-file=.env packages/config/scripts/reset-db.ts',\n },\n devDependencies: {\n turbo: '^2.3.3',\n typescript: '^5.7.3',\n tsx: `^${DEPS.tsx}`,\n 'drizzle-kit': `^${DEPS.drizzleKit}`,\n },\n },\n null,\n 2,\n )}\\n`,\n )\n\n // .gitignore\n await writeFile(\n join(projectDir, '.gitignore'),\n `# dependencies\nnode_modules/\n\n# build\ndist/\n.next/\n.turbo/\n\n# env\n.env*\n!.env.example\n\n# toolkit generated\ngenerated/\n\n# IDE\n.vscode/\n.idea/\n\n# OS\n.DS_Store\nThumbs.db\n\n# logs\n*.log\n\n# migrations meta\nmigrations/\n\n# turbo\n.turbo/\n`,\n )\n\n // .env.example\n await writeFile(\n join(projectDir, '.env.example'),\n `DATABASE_URL=postgresql://${name}:${name}_dev_password@localhost:5432/${name}_dev\nBETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars\nBETTER_AUTH_URL=http://localhost:3000\nLOG_LEVEL=debug\nNEXT_PUBLIC_APP_URL=http://localhost:3000\nQUEUE_WORKER=true\nRESEND_API_KEY=\nRESEND_WEBHOOK_SECRET=\nMAIL_FROM=noreply@example.com\nCSAT_SECRET=\n`,\n )\n\n // docker-compose.yml\n await writeFile(\n join(projectDir, 'docker-compose.yml'),\n `services:\n postgres:\n image: postgres:17-alpine\n container_name: ${name}-postgres\n restart: unless-stopped\n environment:\n POSTGRES_USER: ${name}\n POSTGRES_PASSWORD: ${name}_dev_password\n POSTGRES_DB: ${name}_dev\n ports:\n - \"5432:5432\"\n volumes:\n - postgres_data:/var/lib/postgresql/data\n healthcheck:\n test: [\"CMD-SHELL\", \"pg_isready -U ${name} -d ${name}_dev\"]\n interval: 10s\n timeout: 5s\n retries: 5\n\nvolumes:\n postgres_data:\n name: ${name}_postgres_data\n`,\n )\n}\n\n// ──────────────────────────────────────────────────────\n// Shared config package (packages/config/)\n// ──────────────────────────────────────────────────────\n\nasync function generateSharedConfig(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n const configDir = join(projectDir, 'packages/config')\n\n // package.json\n await writeFile(\n join(configDir, 'package.json'),\n `${JSON.stringify(\n {\n name: `@${name}/config`,\n version: '0.1.0',\n private: true,\n type: 'module',\n exports: {\n '.': './toolkit.config.ts',\n './entities': './entities/index.ts',\n './entities/*': './entities/*',\n './app': './lib/app.ts',\n './auth': './lib/auth.ts',\n './auth-client': './lib/auth-client.ts',\n },\n dependencies: {\n '@murumets-ee/core': `^${DEPS.myorgCore}`,\n '@murumets-ee/db': `^${DEPS.myorgDb}`,\n '@murumets-ee/entity': `^${DEPS.myorgEntity}`,\n '@murumets-ee/logging': `^${DEPS.myorgLogging}`,\n '@murumets-ee/auth': `^${DEPS.myorgAuth}`,\n 'better-auth': `^${DEPS.betterAuth}`,\n 'drizzle-orm': `^${DEPS.drizzleOrm}`,\n postgres: `^${DEPS.postgres}`,\n zod: `^${DEPS.zod}`,\n },\n },\n null,\n 2,\n )}\\n`,\n )\n\n // tsconfig.json (for editor support, no build)\n await writeFile(\n join(configDir, 'tsconfig.json'),\n `${JSON.stringify(\n {\n compilerOptions: {\n target: 'ES2022',\n module: 'ESNext',\n moduleResolution: 'Bundler',\n strict: true,\n esModuleInterop: true,\n skipLibCheck: true,\n resolveJsonModule: true,\n },\n include: ['**/*.ts'],\n },\n null,\n 2,\n )}\\n`,\n )\n\n // toolkit.config.ts\n await writeFile(\n join(configDir, 'toolkit.config.ts'),\n `import { auth } from '@murumets-ee/auth/plugin'\nimport { content } from '@murumets-ee/content/plugin'\nimport { defineConfig } from '@murumets-ee/core'\nimport { logging } from '@murumets-ee/logging/plugin'\nimport { mail, ResendMailProvider } from '@murumets-ee/mail'\nimport { media } from '@murumets-ee/media/plugin'\nimport { queue } from '@murumets-ee/queue/plugin'\nimport { settings } from '@murumets-ee/settings/plugin'\nimport { storage } from '@murumets-ee/storage/plugin'\nimport { taxonomy } from '@murumets-ee/taxonomy/plugin'\nimport { ticketing } from '@murumets-ee/ticketing/plugin'\nimport { Article, Category } from './entities'\nimport * as authSchema from './generated/auth-schema'\n\nif (!process.env.DATABASE_URL) {\n throw new Error('DATABASE_URL environment variable is required')\n}\n\nexport default defineConfig({\n db: {\n url: process.env.DATABASE_URL,\n poolMin: 2,\n poolMax: 10,\n },\n logging: {\n level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',\n name: '${name}',\n },\n entities: [Category, Article],\n plugins: [\n auth({ providers: ['email'], schema: authSchema }),\n content({\n locales: [{ code: 'en', label: 'English' }],\n defaultLocale: 'en',\n }),\n logging(),\n settings(),\n storage(),\n media(),\n taxonomy(),\n queue(),\n mail({\n provider: process.env.RESEND_API_KEY\n ? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })\n : undefined,\n defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',\n webhookSecret: process.env.RESEND_WEBHOOK_SECRET,\n }),\n ticketing({\n csatSecret: process.env.CSAT_SECRET,\n }),\n ],\n projectRoot: import.meta.dirname,\n})\n`,\n )\n\n // auth.config.ts\n await writeFile(\n join(configDir, 'auth.config.ts'),\n `import { betterAuth } from 'better-auth'\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle'\nimport { admin } from 'better-auth/plugins'\nimport { organization } from 'better-auth/plugins/organization'\nimport { drizzle } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\n\nconst sql = postgres(process.env.DATABASE_URL!)\nconst db = drizzle(sql)\n\nexport const auth = betterAuth({\n database: drizzleAdapter(db, { provider: 'pg' }),\n emailAndPassword: { enabled: true },\n plugins: [\n admin(),\n organization(),\n ],\n})\n`,\n )\n\n // drizzle.config.ts\n await writeFile(\n join(configDir, 'drizzle.config.ts'),\n `import type { Config } from 'drizzle-kit'\n\nif (!process.env.DATABASE_URL) {\n throw new Error('DATABASE_URL environment variable is required')\n}\n\nexport default {\n schema: ['./generated/schema.ts', './generated/auth-schema.ts'],\n out: './migrations',\n dialect: 'postgresql',\n dbCredentials: {\n url: process.env.DATABASE_URL,\n },\n} satisfies Config\n`,\n )\n\n // entities\n await writeFile(\n join(configDir, 'entities/index.ts'),\n `export { Article } from './article'\nexport { Category } from './category'\n`,\n )\n\n await writeFile(\n join(configDir, 'entities/article.ts'),\n `import { behavior, defineEntity, field } from '@murumets-ee/entity'\n\nexport const Article = defineEntity({\n name: 'article',\n fields: {\n title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),\n slug: field.slug({ from: 'title', unique: true }),\n excerpt: field.text({ maxLength: 500, translatable: true }),\n body: field.richtext(),\n viewCount: field.number({ default: 0, integer: true }),\n featured: field.boolean({ default: false }),\n publishDate: field.date(),\n contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),\n category: field.reference({ entity: 'category', required: false }),\n tags: field.reference({ entity: 'category', cardinality: 'many' }),\n coverImage: field.media({ accept: ['image/*'] }),\n },\n behaviors: [\n behavior.publishable(),\n behavior.auditable(),\n behavior.sluggable('title'),\n behavior.revisionable(),\n ],\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n`,\n )\n\n await writeFile(\n join(configDir, 'entities/category.ts'),\n `import { defineEntity, field } from '@murumets-ee/entity'\n\nexport const Category = defineEntity({\n name: 'category',\n fields: {\n name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),\n slug: field.slug({ from: 'name', unique: true }),\n description: field.text({ maxLength: 500, translatable: true }),\n },\n scope: 'global',\n access: {\n view: 'public',\n create: 'group.editor',\n update: 'group.editor',\n delete: 'group.admin',\n },\n})\n`,\n )\n\n // lib\n await writeFile(\n join(configDir, 'lib/app.ts'),\n `import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'\nimport config from '../toolkit.config'\n\nlet appInstance: ToolkitApp | null = null\n\nexport async function getToolkitApp(): Promise<ToolkitApp> {\n if (!appInstance) {\n appInstance = await createApp(config)\n setApp(appInstance)\n }\n return appInstance\n}\n`,\n )\n\n await writeFile(\n join(configDir, 'lib/auth.ts'),\n `import { getToolkitApp } from './app'\nimport { getAuth } from '@murumets-ee/auth'\n\nexport async function getAuthInstance() {\n await getToolkitApp()\n return getAuth()\n}\n`,\n )\n\n await writeFile(\n join(configDir, 'lib/auth-client.ts'),\n `import { createClient } from '@murumets-ee/auth/client'\n\nexport const authClient = createClient()\n`,\n )\n\n // generated placeholder\n await writeFile(\n join(configDir, 'generated/auth-schema.ts'),\n `// This file is generated by better-auth CLI.\n// Run: npx @better-auth/cli generate --config auth.config.ts -y\nexport {}\n`,\n )\n\n // scripts\n await writeFile(\n join(configDir, 'scripts/generate-schema.ts'),\n `/**\n * Generate Drizzle schemas from entity definitions\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { generateSchemaCode, generateTranslationSchemaCode } from '@murumets-ee/entity'\nimport config from '../toolkit.config'\n\nasync function generateSchemas() {\n console.log('Generating Drizzle schemas from entity definitions...')\n\n const schemaDir = join(import.meta.dirname, '..', 'generated')\n await mkdir(schemaDir, { recursive: true })\n\n const imports: string[] = []\n const schemas: string[] = []\n let hasTranslations = false\n\n for (const entity of config.entities) {\n console.log(\\` - Generating schema for \\${entity.name}\\`)\n\n const schemaCode = generateSchemaCode(entity)\n schemas.push(\\`\\\\n// \\${entity.name} table\\`)\n schemas.push(schemaCode)\n\n const translationCode = generateTranslationSchemaCode(entity)\n if (translationCode) {\n hasTranslations = true\n schemas.push(\\`\\\\n// \\${entity.name} translations\\`)\n schemas.push(translationCode)\n }\n }\n\n const pgCoreTypes = ['pgTable', 'varchar', 'text', 'boolean', 'timestamp', 'integer', 'doublePrecision', 'jsonb', 'uuid', 'index']\n if (hasTranslations) pgCoreTypes.push('unique')\n const pgCoreImports = \\`import { \\${pgCoreTypes.join(', ')} } from 'drizzle-orm/pg-core'\\\\n\\`\n imports.push(pgCoreImports)\n\n const schemaFile = join(schemaDir, 'schema.ts')\n await writeFile(schemaFile, [...imports, ...schemas].join('\\\\n'))\n\n console.log(\\`\\\\nSchemas written to: \\${schemaFile}\\`)\n console.log('Run \\\\\\`pnpm db:migrate:generate\\\\\\` to create migrations')\n}\n\ngenerateSchemas().catch(console.error)\n`,\n )\n\n await writeFile(\n join(configDir, 'scripts/migrate.ts'),\n `/**\n * Run pending migrations\n */\n\nimport { createDbClient, runMigrations } from '@murumets-ee/db'\n\nasync function migrate() {\n console.log('Running migrations...')\n\n try {\n if (!process.env.DATABASE_URL) {\n throw new Error('DATABASE_URL environment variable is required')\n }\n\n const db = createDbClient({ url: process.env.DATABASE_URL })\n await runMigrations(db, import.meta.dirname + '/..')\n\n console.log('Migrations completed successfully')\n process.exit(0)\n } catch (error) {\n console.error('Migration failed:', error)\n process.exit(1)\n }\n}\n\nmigrate()\n`,\n )\n\n await writeFile(\n join(configDir, 'scripts/reset-db.ts'),\n `/**\n * Reset database (DROP all tables)\n * WARNING: Only for development\n */\n\nimport postgres from 'postgres'\n\nasync function resetDb() {\n const DATABASE_URL = process.env.DATABASE_URL\n\n if (!DATABASE_URL) {\n throw new Error('DATABASE_URL not set')\n }\n\n console.warn('WARNING: This will DROP ALL TABLES')\n console.log('Database:', DATABASE_URL.split('@')[1])\n\n const sql = postgres(DATABASE_URL)\n\n try {\n await sql\\`\n DROP SCHEMA public CASCADE;\n CREATE SCHEMA public;\n GRANT ALL ON SCHEMA public TO PUBLIC;\n \\`\n\n console.log('Database reset complete')\n } finally {\n await sql.end()\n }\n}\n\nresetDb().catch(console.error)\n`,\n )\n}\n\n// ──────────────────────────────────────────────────────\n// Admin app (apps/admin/) — full app with auth\n// ──────────────────────────────────────────────────────\n\nasync function createAndOverlayAdmin(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n const adminDir = join(projectDir, 'apps/admin')\n\n // Run create-next-app\n await mkdir(join(projectDir, 'apps'), { recursive: true })\n runCommand(\n `pnpm create next-app@${CREATE_NEXT_APP_VERSION} ${adminDir} ` +\n '--typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias \"@/*\"',\n )\n\n // Clean up create-next-app artifacts\n await deleteFiles(adminDir, [\n 'app/page.tsx',\n 'app/page.module.css',\n 'app/fonts',\n 'README.md',\n 'pnpm-workspace.yaml', // root workspace handles this\n ])\n\n // Merge package.json\n await mergePackageJson(join(adminDir, 'package.json'), {\n dependencies: {\n [`@${name}/config`]: 'workspace:*',\n '@murumets-ee/auth-ui': `^${DEPS.myorgAuthUi}`,\n 'better-auth': `^${DEPS.betterAuth}`,\n 'next-intl': `^${DEPS.nextIntl}`,\n 'next-themes': `^${DEPS.nextThemes}`,\n 'lucide-react': `^${DEPS.lucideReact}`,\n 'react-hook-form': `^${DEPS.reactHookForm}`,\n '@hookform/resolvers': `^${DEPS.hookformResolvers}`,\n zod: `^${DEPS.zod}`,\n },\n devDependencies: {\n 'babel-plugin-react-compiler': DEPS.babelReactCompiler,\n },\n })\n\n // .gitignore augmentation\n const { appendToFile } = await import('../utils')\n await appendToFile(join(adminDir, '.gitignore'), '\\n# Environment\\n.env*\\n!.env.example\\n')\n\n // next.config.ts — includes shared config package in transpilePackages\n await writeFile(\n join(adminDir, 'next.config.ts'),\n `import type { NextConfig } from 'next'\nimport createNextIntlPlugin from 'next-intl/plugin'\n\nconst withNextIntl = createNextIntlPlugin('./i18n/request.ts')\n\nconst nextConfig: NextConfig = {\n reactCompiler: true,\n transpilePackages: [\n '@${name}/config',\n '@murumets-ee/core',\n '@murumets-ee/entity',\n '@murumets-ee/db',\n '@murumets-ee/logging',\n '@murumets-ee/auth-ui',\n ],\n serverExternalPackages: ['drizzle-orm', 'postgres'],\n experimental: {\n serverActions: {\n bodySizeLimit: '2mb',\n },\n },\n}\n\nexport default withNextIntl(nextConfig)\n`,\n )\n\n // proxy.ts (with auth)\n await writeFile(\n join(adminDir, 'proxy.ts'),\n `import { NextRequest, NextResponse } from 'next/server'\nimport { getSessionCookie } from 'better-auth/cookies'\nimport createMiddleware from 'next-intl/middleware'\nimport { routing } from './i18n/routing'\n\nconst intlMiddleware = createMiddleware(routing)\n\nconst protectedPaths = ['/setup']\n\nexport function proxy(request: NextRequest) {\n const { pathname } = request.nextUrl\n\n const localePattern = new RegExp(\\`^/(\\${routing.locales.join('|')})\\`)\n const pathWithoutLocale = pathname.replace(localePattern, '') || '/'\n\n if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {\n const session = getSessionCookie(request)\n if (!session) {\n const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale\n return NextResponse.redirect(new URL(\\`/\\${locale}/auth/sign-in\\`, request.url))\n }\n }\n\n return intlMiddleware(request)\n}\n\nexport const config = {\n matcher: '/((?!api|_next|_vercel|.*\\\\\\\\..*).*)',\n}\n`,\n )\n\n // i18n\n await writeFile(\n join(adminDir, 'i18n/routing.ts'),\n `import { defineRouting } from 'next-intl/routing'\n\nexport const routing = defineRouting({\n locales: ['en'],\n defaultLocale: 'en',\n})\n`,\n )\n\n await writeFile(\n join(adminDir, 'i18n/request.ts'),\n `import { getRequestConfig } from 'next-intl/server'\nimport { hasLocale } from 'next-intl'\nimport { routing } from './routing'\nimport { getAuthMessages } from '@murumets-ee/auth-ui/i18n'\n\nexport default getRequestConfig(async ({ requestLocale }) => {\n const requested = await requestLocale\n const locale = hasLocale(routing.locales, requested)\n ? requested\n : routing.defaultLocale\n\n const authMessages = await getAuthMessages(locale)\n\n return {\n locale,\n messages: {\n ...authMessages,\n },\n }\n})\n`,\n )\n\n // Theme files\n await writeFile(\n join(adminDir, 'app/globals.css'),\n `@import \"tailwindcss\";\n@source \"../node_modules/@murumets-ee/auth-ui/dist\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n:root {\n --background: #ffffff;\n --foreground: #171717;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n}\n\n.dark {\n --background: #0a0a0a;\n --foreground: #ededed;\n}\n\nbody {\n background: var(--background);\n color: var(--foreground);\n font-family: Arial, Helvetica, sans-serif;\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/theme-provider.tsx'),\n `'use client'\n\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\nimport type { ReactNode } from 'react'\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n return (\n <NextThemesProvider\n attribute=\"class\"\n defaultTheme=\"system\"\n enableSystem\n disableTransitionOnChange\n >\n {children}\n </NextThemesProvider>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/theme-toggle.tsx'),\n `'use client'\n\nimport { useTheme } from 'next-themes'\nimport { useState, useEffect } from 'react'\nimport { Sun, Moon } from 'lucide-react'\n\nexport function ThemeToggle() {\n const [mounted, setMounted] = useState(false)\n const { resolvedTheme, setTheme } = useTheme()\n\n useEffect(() => { setMounted(true) }, [])\n\n if (!mounted) {\n return <div className=\"h-8 w-8\" />\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}\n 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\"\n aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}\n >\n {resolvedTheme === 'dark' ? <Sun className=\"h-5 w-5\" /> : <Moon className=\"h-5 w-5\" />}\n </button>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/nav-header.tsx'),\n `'use client'\n\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { ThemeToggle } from './theme-toggle'\n\nconst links = [\n { href: '/', label: 'Home' },\n { href: '/auth/sign-in', label: 'Sign In' },\n { href: '/setup', label: 'Setup' },\n]\n\nexport function NavHeader() {\n const pathname = usePathname()\n\n function isActive(href: string) {\n return href === '/' ? pathname === '/' : pathname.startsWith(href)\n }\n\n const linkClass = (href: string) =>\n \\`px-3 py-1.5 rounded-md text-sm transition-colors \\${\n isActive(href)\n ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'\n : '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'\n }\\`\n\n return (\n <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\">\n <nav className=\"max-w-6xl mx-auto flex items-center gap-1 px-4 h-12\">\n <span className=\"font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none\">\n ${name} Admin\n </span>\n {links.map(({ href, label }) => (\n <Link key={href} href={href} className={linkClass(href)}>\n {label}\n </Link>\n ))}\n <div className=\"ml-auto\">\n <ThemeToggle />\n </div>\n </nav>\n </header>\n )\n}\n`,\n )\n\n // Layouts\n await writeFile(\n join(adminDir, 'app/layout.tsx'),\n `import './globals.css'\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return children\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/layout.tsx'),\n `import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { NextIntlClientProvider } from 'next-intl'\nimport { getMessages } from 'next-intl/server'\nimport { notFound } from 'next/navigation'\nimport { routing } from '@/i18n/routing'\nimport { ThemeProvider } from '../theme-provider'\nimport { NavHeader } from '../nav-header'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '${name} Admin',\n description: 'Built with Lumi CMS Toolkit',\n}\n\nexport default async function LocaleLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise<{ locale: string }>\n}) {\n const { locale } = await params\n if (!routing.locales.includes(locale as any)) notFound()\n\n const messages = await getMessages()\n\n return (\n <html lang={locale} suppressHydrationWarning>\n <body className={\\`\\${geistSans.variable} \\${geistMono.variable} antialiased\\`}>\n <ThemeProvider>\n <NextIntlClientProvider locale={locale} messages={messages}>\n <NavHeader />\n {children}\n </NextIntlClientProvider>\n </ThemeProvider>\n </body>\n </html>\n )\n}\n`,\n )\n\n // Home page — imports from @name/config\n await writeFile(\n join(adminDir, 'app/[locale]/page.tsx'),\n `import { createQueryClient } from '@murumets-ee/core/clients'\nimport { Article, Category } from '@${name}/config/entities'\nimport { getToolkitApp } from '@${name}/config/app'\n\nexport default async function HomePage() {\n await getToolkitApp()\n const articles = createQueryClient(Article)\n const categories = createQueryClient(Category)\n\n const [articleList, categoryList] = await Promise.all([\n articles.findMany({ limit: 10 }),\n categories.findMany({}),\n ])\n\n return (\n <div className=\"min-h-screen p-8\">\n <div className=\"max-w-4xl mx-auto\">\n <h1 className=\"text-4xl font-bold mb-8\">Welcome to ${name}</h1>\n\n <section className=\"mb-12\">\n <h2 className=\"text-2xl font-semibold mb-4\">\n Categories ({categoryList.length})\n </h2>\n <div className=\"grid gap-4 md:grid-cols-2\">\n {categoryList.map((cat) => (\n <div\n key={cat.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-4\"\n >\n <h3 className=\"font-semibold text-lg\">{cat.name}</h3>\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {cat.description}\n </p>\n </div>\n ))}\n </div>\n </section>\n\n <section>\n <h2 className=\"text-2xl font-semibold mb-4\">\n Articles ({articleList.length})\n </h2>\n {articleList.length === 0 ? (\n <p className=\"text-zinc-500\">No published articles yet.</p>\n ) : (\n <div className=\"space-y-4\">\n {articleList.map((article) => (\n <div\n key={article.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-6\"\n >\n <h3 className=\"text-xl font-bold\">{article.title}</h3>\n <p className=\"text-zinc-600 dark:text-zinc-400 mt-2\">\n {article.excerpt}\n </p>\n </div>\n ))}\n </div>\n )}\n </section>\n </div>\n </div>\n )\n}\n`,\n )\n\n // Auth API route — imports from @name/config\n await writeFile(\n join(adminDir, 'app/api/auth/[...all]/route.ts'),\n `import { toNextJsHandler } from 'better-auth/next-js'\nimport { getAuthInstance } from '@${name}/config/auth'\n\nlet _handler: ReturnType<typeof toNextJsHandler> | null = null\n\nasync function handler() {\n if (!_handler) {\n const auth = await getAuthInstance()\n _handler = toNextJsHandler(auth)\n }\n return _handler\n}\n\nexport async function GET(req: Request) {\n return (await handler()).GET(req)\n}\n\nexport async function POST(req: Request) {\n return (await handler()).POST(req)\n}\n`,\n )\n\n // Auth pages\n await writeFile(\n join(adminDir, 'app/[locale]/auth/layout.tsx'),\n `import { AuthProviders } from './providers'\nimport type { ReactNode } from 'react'\n\nexport default function AuthLayout({ children }: { children: ReactNode }) {\n return (\n <AuthProviders>\n <div className=\"flex min-h-[calc(100vh-3rem)] items-center justify-center px-4\">\n {children}\n </div>\n </AuthProviders>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/providers.tsx'),\n `'use client'\n\nimport { AuthUIProvider } from '@murumets-ee/auth-ui'\nimport { authClient } from '@${name}/config/auth-client'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useLocale } from 'next-intl'\nimport type { ReactNode } from 'react'\n\nexport function AuthProviders({ children }: { children: ReactNode }) {\n const router = useRouter()\n const locale = useLocale()\n\n return (\n <AuthUIProvider\n authClient={authClient as never}\n basePath=\"/auth\"\n redirectTo=\"/\"\n Link={Link}\n navigate={(url) => router.push(url)}\n resetPasswordUrl={\\`/\\${locale}/auth/reset-password\\`}\n >\n {children}\n </AuthUIProvider>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/sign-in/page.tsx'),\n `import { SignInForm } from '@murumets-ee/auth-ui'\n\nexport default function SignInPage() {\n return <SignInForm />\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/sign-up/page.tsx'),\n `import { SignUpForm } from '@murumets-ee/auth-ui'\n\nexport default function SignUpPage() {\n return <SignUpForm />\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/forgot-password/page.tsx'),\n `import { ForgotPasswordForm } from '@murumets-ee/auth-ui'\n\nexport default function ForgotPasswordPage() {\n return <ForgotPasswordForm />\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/reset-password/page.tsx'),\n `import { Suspense } from 'react'\nimport { ResetPasswordContent } from './content'\n\nexport default function ResetPasswordPage() {\n return (\n <Suspense>\n <ResetPasswordContent />\n </Suspense>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/auth/reset-password/content.tsx'),\n `'use client'\n\nimport { useSearchParams } from 'next/navigation'\nimport { ResetPasswordForm } from '@murumets-ee/auth-ui'\n\nexport function ResetPasswordContent() {\n const searchParams = useSearchParams()\n const token = searchParams.get('token')\n\n if (!token) {\n return (\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-bold text-zinc-900 dark:text-zinc-50\">\n Invalid or expired link\n </h1>\n <p className=\"mt-2 text-zinc-500\">\n Please request a new password reset.\n </p>\n </div>\n )\n }\n\n return <ResetPasswordForm token={token} />\n}\n`,\n )\n\n // Setup flow — imports from @name/config\n await writeFile(\n join(adminDir, 'app/[locale]/setup/page.tsx'),\n `import { redirect } from 'next/navigation'\nimport { sql } from 'drizzle-orm'\nimport { getToolkitApp } from '@${name}/config/app'\nimport { SetupForm } from './form'\n\nexport default async function SetupPage() {\n const app = await getToolkitApp()\n const result = await app.db.readOnly.execute<{ count: string }>(\n sql\\`SELECT COUNT(*)::text as count FROM \"user\"\\`,\n )\n\n if (Number(result[0]?.count) > 0) {\n redirect('/')\n }\n\n return (\n <div className=\"max-w-md mx-auto p-8\">\n <h1 className=\"text-2xl font-bold mb-2\">Create Admin</h1>\n <p className=\"text-sm text-zinc-500 dark:text-zinc-400 mb-6\">\n No users found. Create the first admin account.\n </p>\n <SetupForm />\n </div>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/setup/form.tsx'),\n `'use client'\n\nimport { useState } from 'react'\nimport { createFirstAdmin } from './actions'\n\nexport function SetupForm() {\n const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)\n const [loading, setLoading] = useState(false)\n\n async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {\n e.preventDefault()\n setLoading(true)\n setResult(null)\n const res = await createFirstAdmin(new FormData(e.currentTarget))\n setResult(res)\n setLoading(false)\n }\n\n if (result?.ok) {\n return (\n <div className=\"space-y-2\">\n <p className=\"text-green-600 dark:text-green-400\">Admin created: {result.email}</p>\n <a href=\"/auth/sign-in\" className=\"text-blue-600 dark:text-blue-400 hover:underline\">\n Sign in &rarr;\n </a>\n </div>\n )\n }\n\n return (\n <form onSubmit={handleSubmit} className=\"flex flex-col gap-4\">\n <label className=\"text-sm font-medium\">\n Name\n <input\n name=\"name\"\n required\n 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\"\n />\n </label>\n <label className=\"text-sm font-medium\">\n Email\n <input\n name=\"email\"\n type=\"email\"\n required\n 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\"\n />\n </label>\n <label className=\"text-sm font-medium\">\n Password (min 8 chars)\n <input\n name=\"password\"\n type=\"password\"\n required\n minLength={8}\n 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\"\n />\n </label>\n\n {result?.error && <p className=\"text-red-600 dark:text-red-400 text-sm\">{result.error}</p>}\n\n <button\n type=\"submit\"\n disabled={loading}\n className=\"px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors\"\n >\n {loading ? 'Creating...' : 'Create Admin'}\n </button>\n </form>\n )\n}\n`,\n )\n\n await writeFile(\n join(adminDir, 'app/[locale]/setup/actions.ts'),\n `'use server'\n\nimport { sql } from 'drizzle-orm'\nimport { getToolkitApp } from '@${name}/config/app'\nimport { getAuth } from '@murumets-ee/auth'\n\nexport async function createFirstAdmin(formData: FormData) {\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n const name = formData.get('name') as string\n\n if (!email || !password || !name) {\n return { error: 'All fields are required' }\n }\n\n if (password.length < 8) {\n return { error: 'Password must be at least 8 characters' }\n }\n\n const app = await getToolkitApp()\n\n const canCreate = await app.db.readWrite.transaction(async (tx) => {\n await tx.execute(sql\\`SELECT pg_advisory_xact_lock(1)\\`)\n\n const result = await tx.execute<{ count: string }>(\n sql\\`SELECT COUNT(*)::text as count FROM \"user\"\\`,\n )\n return Number(result[0]?.count) === 0\n })\n\n if (!canCreate) {\n return { error: 'Admin user already exists. Setup is complete.' }\n }\n\n const auth = getAuth()\n\n const adminUser = await auth.api.signUpEmail({\n body: { email, password, name },\n })\n\n await app.db.readWrite.execute(\n sql\\`UPDATE \"user\" SET role = 'admin' WHERE id = \\${adminUser.user.id}\\`,\n )\n\n return { ok: true, email: adminUser.user.email }\n}\n`,\n )\n}\n\n// ──────────────────────────────────────────────────────\n// Web app (apps/web/) — public frontend, no auth\n// ──────────────────────────────────────────────────────\n\nasync function createAndOverlayWeb(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n const webDir = join(projectDir, 'apps/web')\n\n // Run create-next-app\n runCommand(\n `pnpm create next-app@${CREATE_NEXT_APP_VERSION} ${webDir} ` +\n '--typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias \"@/*\"',\n )\n\n // Clean up\n await deleteFiles(webDir, [\n 'app/page.tsx',\n 'app/page.module.css',\n 'app/fonts',\n 'README.md',\n 'pnpm-workspace.yaml', // root workspace handles this\n ])\n\n // Merge package.json — lighter deps\n await mergePackageJson(join(webDir, 'package.json'), {\n dependencies: {\n [`@${name}/config`]: 'workspace:*',\n '@murumets-ee/core': `^${DEPS.myorgCore}`,\n '@murumets-ee/auth-ui': `^${DEPS.myorgAuthUi}`,\n 'next-intl': `^${DEPS.nextIntl}`,\n 'next-themes': `^${DEPS.nextThemes}`,\n 'lucide-react': `^${DEPS.lucideReact}`,\n },\n devDependencies: {\n 'babel-plugin-react-compiler': DEPS.babelReactCompiler,\n },\n })\n\n // .gitignore augmentation\n const { appendToFile } = await import('../utils')\n await appendToFile(join(webDir, '.gitignore'), '\\n# Environment\\n.env*\\n!.env.example\\n')\n\n // next.config.ts\n await writeFile(\n join(webDir, 'next.config.ts'),\n `import type { NextConfig } from 'next'\nimport createNextIntlPlugin from 'next-intl/plugin'\n\nconst withNextIntl = createNextIntlPlugin('./i18n/request.ts')\n\nconst nextConfig: NextConfig = {\n reactCompiler: true,\n transpilePackages: [\n '@${name}/config',\n '@murumets-ee/core',\n '@murumets-ee/entity',\n '@murumets-ee/db',\n '@murumets-ee/logging',\n '@murumets-ee/auth-ui',\n ],\n serverExternalPackages: ['drizzle-orm', 'postgres'],\n}\n\nexport default withNextIntl(nextConfig)\n`,\n )\n\n // proxy.ts — intl only, no auth\n await writeFile(\n join(webDir, 'proxy.ts'),\n `import createMiddleware from 'next-intl/middleware'\nimport { routing } from './i18n/routing'\n\nexport default createMiddleware(routing)\n\nexport const config = {\n matcher: '/((?!api|_next|_vercel|.*\\\\\\\\..*).*)',\n}\n`,\n )\n\n // i18n\n await writeFile(\n join(webDir, 'i18n/routing.ts'),\n `import { defineRouting } from 'next-intl/routing'\n\nexport const routing = defineRouting({\n locales: ['en'],\n defaultLocale: 'en',\n})\n`,\n )\n\n await writeFile(\n join(webDir, 'i18n/request.ts'),\n `import { getRequestConfig } from 'next-intl/server'\nimport { hasLocale } from 'next-intl'\nimport { routing } from './routing'\nimport { getAuthMessages } from '@murumets-ee/auth-ui/i18n'\n\nexport default getRequestConfig(async ({ requestLocale }) => {\n const requested = await requestLocale\n const locale = hasLocale(routing.locales, requested)\n ? requested\n : routing.defaultLocale\n\n const authMessages = await getAuthMessages(locale)\n\n return {\n locale,\n messages: {\n ...authMessages,\n },\n }\n})\n`,\n )\n\n // Theme files\n await writeFile(\n join(webDir, 'app/globals.css'),\n `@import \"tailwindcss\";\n@source \"../node_modules/@murumets-ee/auth-ui/dist\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n:root {\n --background: #ffffff;\n --foreground: #171717;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n}\n\n.dark {\n --background: #0a0a0a;\n --foreground: #ededed;\n}\n\nbody {\n background: var(--background);\n color: var(--foreground);\n font-family: Arial, Helvetica, sans-serif;\n}\n`,\n )\n\n await writeFile(\n join(webDir, 'app/theme-provider.tsx'),\n `'use client'\n\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\nimport type { ReactNode } from 'react'\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n return (\n <NextThemesProvider\n attribute=\"class\"\n defaultTheme=\"system\"\n enableSystem\n disableTransitionOnChange\n >\n {children}\n </NextThemesProvider>\n )\n}\n`,\n )\n\n await writeFile(\n join(webDir, 'app/theme-toggle.tsx'),\n `'use client'\n\nimport { useTheme } from 'next-themes'\nimport { useState, useEffect } from 'react'\nimport { Sun, Moon } from 'lucide-react'\n\nexport function ThemeToggle() {\n const [mounted, setMounted] = useState(false)\n const { resolvedTheme, setTheme } = useTheme()\n\n useEffect(() => { setMounted(true) }, [])\n\n if (!mounted) {\n return <div className=\"h-8 w-8\" />\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}\n 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\"\n aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}\n >\n {resolvedTheme === 'dark' ? <Sun className=\"h-5 w-5\" /> : <Moon className=\"h-5 w-5\" />}\n </button>\n )\n}\n`,\n )\n\n // Nav header — simpler, no auth links\n await writeFile(\n join(webDir, 'app/nav-header.tsx'),\n `'use client'\n\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { ThemeToggle } from './theme-toggle'\n\nconst links = [\n { href: '/', label: 'Home' },\n]\n\nexport function NavHeader() {\n const pathname = usePathname()\n\n function isActive(href: string) {\n return href === '/' ? pathname === '/' : pathname.startsWith(href)\n }\n\n const linkClass = (href: string) =>\n \\`px-3 py-1.5 rounded-md text-sm transition-colors \\${\n isActive(href)\n ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'\n : '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'\n }\\`\n\n return (\n <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\">\n <nav className=\"max-w-6xl mx-auto flex items-center gap-1 px-4 h-12\">\n <span className=\"font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none\">\n ${name}\n </span>\n {links.map(({ href, label }) => (\n <Link key={href} href={href} className={linkClass(href)}>\n {label}\n </Link>\n ))}\n <div className=\"ml-auto\">\n <ThemeToggle />\n </div>\n </nav>\n </header>\n )\n}\n`,\n )\n\n // Layouts\n await writeFile(\n join(webDir, 'app/layout.tsx'),\n `import './globals.css'\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return children\n}\n`,\n )\n\n await writeFile(\n join(webDir, 'app/[locale]/layout.tsx'),\n `import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { NextIntlClientProvider } from 'next-intl'\nimport { getMessages } from 'next-intl/server'\nimport { notFound } from 'next/navigation'\nimport { routing } from '@/i18n/routing'\nimport { ThemeProvider } from '../theme-provider'\nimport { NavHeader } from '../nav-header'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '${name}',\n description: 'Built with Lumi CMS Toolkit',\n}\n\nexport default async function LocaleLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise<{ locale: string }>\n}) {\n const { locale } = await params\n if (!routing.locales.includes(locale as any)) notFound()\n\n const messages = await getMessages()\n\n return (\n <html lang={locale} suppressHydrationWarning>\n <body className={\\`\\${geistSans.variable} \\${geistMono.variable} antialiased\\`}>\n <ThemeProvider>\n <NextIntlClientProvider locale={locale} messages={messages}>\n <NavHeader />\n {children}\n </NextIntlClientProvider>\n </ThemeProvider>\n </body>\n </html>\n )\n}\n`,\n )\n\n // Home page — public content, imports from @name/config\n await writeFile(\n join(webDir, 'app/[locale]/page.tsx'),\n `import { createQueryClient } from '@murumets-ee/core/clients'\nimport { Article, Category } from '@${name}/config/entities'\nimport { getToolkitApp } from '@${name}/config/app'\n\nexport default async function HomePage() {\n await getToolkitApp()\n const articles = createQueryClient(Article)\n const categories = createQueryClient(Category)\n\n const [articleList, categoryList] = await Promise.all([\n articles.findMany({ limit: 10 }),\n categories.findMany({}),\n ])\n\n return (\n <div className=\"min-h-screen p-8\">\n <div className=\"max-w-4xl mx-auto\">\n <h1 className=\"text-4xl font-bold mb-8\">Welcome to ${name}</h1>\n\n <section className=\"mb-12\">\n <h2 className=\"text-2xl font-semibold mb-4\">\n Categories ({categoryList.length})\n </h2>\n <div className=\"grid gap-4 md:grid-cols-2\">\n {categoryList.map((cat) => (\n <div\n key={cat.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-4\"\n >\n <h3 className=\"font-semibold text-lg\">{cat.name}</h3>\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {cat.description}\n </p>\n </div>\n ))}\n </div>\n </section>\n\n <section>\n <h2 className=\"text-2xl font-semibold mb-4\">\n Articles ({articleList.length})\n </h2>\n {articleList.length === 0 ? (\n <p className=\"text-zinc-500\">No published articles yet.</p>\n ) : (\n <div className=\"space-y-4\">\n {articleList.map((article) => (\n <div\n key={article.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-6\"\n >\n <h3 className=\"text-xl font-bold\">{article.title}</h3>\n <p className=\"text-zinc-600 dark:text-zinc-400 mt-2\">\n {article.excerpt}\n </p>\n </div>\n ))}\n </div>\n )}\n </section>\n </div>\n </div>\n )\n}\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateNextjs(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n\n // next.config.ts\n await writeFile(\n join(projectDir, 'next.config.ts'),\n `import type { NextConfig } from 'next'\nimport createNextIntlPlugin from 'next-intl/plugin'\n\nconst withNextIntl = createNextIntlPlugin('./i18n/request.ts')\n\nconst nextConfig: NextConfig = {\n output: 'standalone',\n reactCompiler: true,\n transpilePackages: [\n '@murumets-ee/core', '@murumets-ee/entity', '@murumets-ee/db',\n '@murumets-ee/logging', '@murumets-ee/auth', '@murumets-ee/auth-ui',\n '@murumets-ee/admin-ui', '@murumets-ee/content', '@murumets-ee/settings',\n '@murumets-ee/storage', '@murumets-ee/media', '@murumets-ee/taxonomy',\n '@murumets-ee/queue', '@murumets-ee/mail', '@murumets-ee/ticketing',\n '@murumets-ee/ticketing-ui', '@murumets-ee/tokens', '@murumets-ee/ui',\n ],\n serverExternalPackages: ['drizzle-orm', 'postgres', 'pino', 'file-type'],\n experimental: {\n serverActions: {\n bodySizeLimit: '2mb',\n },\n },\n}\n\nexport default withNextIntl(nextConfig)\n`,\n )\n\n // proxy.ts\n await writeFile(\n join(projectDir, 'proxy.ts'),\n `import { NextRequest, NextResponse } from 'next/server'\nimport { getSessionCookie } from 'better-auth/cookies'\nimport createMiddleware from 'next-intl/middleware'\nimport { routing } from './i18n/routing'\n\nconst intlMiddleware = createMiddleware(routing)\n\nconst protectedPaths = ['/setup']\n\nexport function proxy(request: NextRequest) {\n const { pathname } = request.nextUrl\n\n // Strip locale prefix to check actual path\n const localePattern = new RegExp(\\`^/(\\${routing.locales.join('|')})\\`)\n const pathWithoutLocale = pathname.replace(localePattern, '') || '/'\n\n // Auth check for protected paths\n if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {\n const session = getSessionCookie(request)\n if (!session) {\n const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale\n return NextResponse.redirect(new URL(\\`/\\${locale}/auth/sign-in\\`, request.url))\n }\n }\n\n // Locale routing\n return intlMiddleware(request)\n}\n\nexport const config = {\n matcher: '/((?!api|_next|_vercel|.*\\\\\\\\..*).*)',\n}\n`,\n )\n\n // app/layout.tsx (root — thin shell)\n await writeFile(\n join(projectDir, 'app/layout.tsx'),\n `import './globals.css'\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n return children\n}\n`,\n )\n\n // app/[locale]/layout.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/layout.tsx'),\n `import type { Metadata } from 'next'\nimport { Geist, Geist_Mono } from 'next/font/google'\nimport { NextIntlClientProvider } from 'next-intl'\nimport { getMessages } from 'next-intl/server'\nimport { notFound } from 'next/navigation'\nimport { routing } from '@/i18n/routing'\nimport { ThemeProvider } from '../theme-provider'\nimport { NavHeader } from '../nav-header'\n\nconst geistSans = Geist({\n variable: '--font-geist-sans',\n subsets: ['latin'],\n})\n\nconst geistMono = Geist_Mono({\n variable: '--font-geist-mono',\n subsets: ['latin'],\n})\n\nexport const metadata: Metadata = {\n title: '${name}',\n description: 'Built with Lumi CMS Toolkit',\n}\n\nexport default async function LocaleLayout({\n children,\n params,\n}: {\n children: React.ReactNode\n params: Promise<{ locale: string }>\n}) {\n const { locale } = await params\n if (!routing.locales.includes(locale as any)) notFound()\n\n const messages = await getMessages()\n\n return (\n <html lang={locale} suppressHydrationWarning>\n <body className={\\`\\${geistSans.variable} \\${geistMono.variable} antialiased\\`}>\n <ThemeProvider>\n <NextIntlClientProvider locale={locale} messages={messages}>\n <NavHeader />\n {children}\n </NextIntlClientProvider>\n </ThemeProvider>\n </body>\n </html>\n )\n}\n`,\n )\n\n // app/[locale]/page.tsx\n await writeFile(\n join(projectDir, 'app/[locale]/page.tsx'),\n `import { createQueryClient } from '@murumets-ee/core/clients'\nimport { Article, Category } from '@/entities'\nimport { getToolkitApp } from '@/lib/app'\n\nexport default async function HomePage() {\n await getToolkitApp()\n const articles = createQueryClient(Article)\n const categories = createQueryClient(Category)\n\n const [articleList, categoryList] = await Promise.all([\n articles.findMany({ limit: 10 }),\n categories.findMany({}),\n ])\n\n return (\n <div className=\"min-h-screen p-8\">\n <div className=\"max-w-4xl mx-auto\">\n <h1 className=\"text-4xl font-bold mb-8\">Welcome to ${name}</h1>\n\n <section className=\"mb-12\">\n <h2 className=\"text-2xl font-semibold mb-4\">\n Categories ({categoryList.length})\n </h2>\n <div className=\"grid gap-4 md:grid-cols-2\">\n {categoryList.map((cat) => (\n <div\n key={cat.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-4\"\n >\n <h3 className=\"font-semibold text-lg\">{cat.name}</h3>\n <p className=\"text-sm text-zinc-600 dark:text-zinc-400\">\n {cat.description}\n </p>\n </div>\n ))}\n </div>\n </section>\n\n <section>\n <h2 className=\"text-2xl font-semibold mb-4\">\n Articles ({articleList.length})\n </h2>\n {articleList.length === 0 ? (\n <p className=\"text-zinc-500\">No published articles yet.</p>\n ) : (\n <div className=\"space-y-4\">\n {articleList.map((article) => (\n <div\n key={article.id}\n className=\"border border-zinc-200 dark:border-zinc-800 rounded-lg p-6\"\n >\n <h3 className=\"text-xl font-bold\">{article.title}</h3>\n <p className=\"text-zinc-600 dark:text-zinc-400 mt-2\">\n {article.excerpt}\n </p>\n </div>\n ))}\n </div>\n )}\n </section>\n </div>\n </div>\n )\n}\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateScripts(projectDir: string, _options: ProjectOptions): Promise<void> {\n // scripts/generate-schema.ts — delegates to the CLI binary\n await writeFile(\n join(projectDir, 'scripts/generate-schema.ts'),\n `import { execSync } from 'node:child_process'\nexecSync('npx @murumets-ee/cli generate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })\n`,\n )\n\n // scripts/migrate.ts — delegates to the CLI binary\n await writeFile(\n join(projectDir, 'scripts/migrate.ts'),\n `import { execSync } from 'node:child_process'\nexecSync('npx @murumets-ee/cli migrate', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })\n`,\n )\n\n // scripts/reset-db.ts — delegates to the CLI binary\n await writeFile(\n join(projectDir, 'scripts/reset-db.ts'),\n `import { execSync } from 'node:child_process'\nexecSync('npx @murumets-ee/cli reset --force', { stdio: 'inherit', cwd: import.meta.dirname + '/..' })\n`,\n )\n\n // scripts/worker.ts — standalone queue worker process\n await writeFile(\n join(projectDir, 'scripts/worker.ts'),\n `/**\n * Standalone queue worker process.\n *\n * Run separately from the Next.js web server:\n * pnpm worker (or: tsx --env-file=.env scripts/worker.ts)\n *\n * The web server should set QUEUE_WORKER=false so it only enqueues jobs.\n * This process handles job execution.\n */\n\nimport { createApp, setApp } from '@murumets-ee/core'\nimport config from '../toolkit.config'\n\nprocess.env.QUEUE_WORKER = 'true'\n\nconst app = await createApp(config)\nsetApp(app)\n\napp.logger.info('Queue worker process running. Press Ctrl+C to stop.')\n\nconst shutdown = async () => {\n app.logger.info('Shutting down queue worker...')\n\n const worker = (globalThis as Record<symbol, unknown>)[\n Symbol.for('@murumets-ee/queue:worker')\n ] as { stop: () => Promise<void> } | undefined\n\n if (worker) {\n await worker.stop()\n }\n\n process.exit(0)\n}\n\nprocess.on('SIGINT', shutdown)\nprocess.on('SIGTERM', shutdown)\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateTheme(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n\n // app/globals.css\n await writeFile(\n join(projectDir, 'app/globals.css'),\n `@import \"tailwindcss\";\n@source \"../node_modules/@murumets-ee/auth-ui/dist\";\n\n@custom-variant dark (&:where(.dark, .dark *));\n\n:root {\n --background: #ffffff;\n --foreground: #171717;\n}\n\n@theme inline {\n --color-background: var(--background);\n --color-foreground: var(--foreground);\n --font-sans: var(--font-geist-sans);\n --font-mono: var(--font-geist-mono);\n}\n\n.dark {\n --background: #0a0a0a;\n --foreground: #ededed;\n}\n\nbody {\n background: var(--background);\n color: var(--foreground);\n font-family: Arial, Helvetica, sans-serif;\n}\n`,\n )\n\n // app/theme-provider.tsx\n await writeFile(\n join(projectDir, 'app/theme-provider.tsx'),\n `'use client'\n\nimport { ThemeProvider as NextThemesProvider } from 'next-themes'\nimport type { ReactNode } from 'react'\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n return (\n <NextThemesProvider\n attribute=\"class\"\n defaultTheme=\"system\"\n enableSystem\n disableTransitionOnChange\n >\n {children}\n </NextThemesProvider>\n )\n}\n`,\n )\n\n // app/theme-toggle.tsx\n await writeFile(\n join(projectDir, 'app/theme-toggle.tsx'),\n `'use client'\n\nimport { useTheme } from 'next-themes'\nimport { useState, useEffect } from 'react'\nimport { Sun, Moon } from 'lucide-react'\n\nexport function ThemeToggle() {\n const [mounted, setMounted] = useState(false)\n const { resolvedTheme, setTheme } = useTheme()\n\n useEffect(() => { setMounted(true) }, [])\n\n if (!mounted) {\n return <div className=\"h-8 w-8\" />\n }\n\n return (\n <button\n type=\"button\"\n onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}\n 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\"\n aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}\n >\n {resolvedTheme === 'dark' ? <Sun className=\"h-5 w-5\" /> : <Moon className=\"h-5 w-5\" />}\n </button>\n )\n}\n`,\n )\n\n // app/nav-header.tsx\n await writeFile(\n join(projectDir, 'app/nav-header.tsx'),\n `'use client'\n\nimport Link from 'next/link'\nimport { usePathname } from 'next/navigation'\nimport { ThemeToggle } from './theme-toggle'\n\nconst links = [\n { href: '/', label: 'Home' },\n { href: '/auth/sign-in', label: 'Sign In' },\n { href: '/setup', label: 'Setup' },\n]\n\nexport function NavHeader() {\n const pathname = usePathname()\n\n function isActive(href: string) {\n return href === '/' ? pathname === '/' : pathname.startsWith(href)\n }\n\n const linkClass = (href: string) =>\n \\`px-3 py-1.5 rounded-md text-sm transition-colors \\${\n isActive(href)\n ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'\n : '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'\n }\\`\n\n return (\n <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\">\n <nav className=\"max-w-6xl mx-auto flex items-center gap-1 px-4 h-12\">\n <span className=\"font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none\">\n ${name}\n </span>\n {links.map(({ href, label }) => (\n <Link key={href} href={href} className={linkClass(href)}>\n {label}\n </Link>\n ))}\n <div className=\"ml-auto\">\n <ThemeToggle />\n </div>\n </nav>\n </header>\n )\n}\n`,\n )\n}\n","import { join } from 'node:path'\nimport type { ProjectOptions } from '../types'\nimport { writeFile } from '../utils'\n\nexport async function generateToolkit(projectDir: string, options: ProjectOptions): Promise<void> {\n const { name } = options\n\n // toolkit.config.ts\n await writeFile(\n join(projectDir, 'toolkit.config.ts'),\n `import { auth } from '@murumets-ee/auth/plugin'\nimport { content } from '@murumets-ee/content/plugin'\nimport { defineConfig } from '@murumets-ee/core'\nimport { logging } from '@murumets-ee/logging/plugin'\nimport { mail, ResendMailProvider } from '@murumets-ee/mail'\nimport { media } from '@murumets-ee/media/plugin'\nimport { queue } from '@murumets-ee/queue/plugin'\nimport { settings } from '@murumets-ee/settings/plugin'\nimport { storage } from '@murumets-ee/storage/plugin'\nimport { taxonomy } from '@murumets-ee/taxonomy/plugin'\nimport { ticketing } from '@murumets-ee/ticketing/plugin'\nimport { Article, Category } from './entities'\nimport * as authSchema from './generated/auth-schema'\n\nif (!process.env.DATABASE_URL) {\n throw new Error('DATABASE_URL environment variable is required')\n}\n\nexport default defineConfig({\n db: {\n url: process.env.DATABASE_URL,\n poolMin: 2,\n poolMax: 10,\n },\n logging: {\n level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',\n name: '${name}',\n },\n entities: [Category, Article],\n plugins: [\n auth({ providers: ['email'], schema: authSchema }),\n content({\n locales: [{ code: 'en', label: 'English' }],\n defaultLocale: 'en',\n }),\n logging(),\n settings(),\n storage(),\n media(),\n taxonomy(),\n queue(),\n mail({\n provider: process.env.RESEND_API_KEY\n ? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })\n : undefined,\n defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',\n webhookSecret: process.env.RESEND_WEBHOOK_SECRET,\n }),\n ticketing({\n csatSecret: process.env.CSAT_SECRET,\n }),\n ],\n projectRoot: import.meta.dirname,\n})\n`,\n )\n\n // lib/app.ts\n await writeFile(\n join(projectDir, 'lib/app.ts'),\n `import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'\nimport config from '../toolkit.config'\n\nlet appInstance: ToolkitApp | null = null\n\nexport async function getToolkitApp(): Promise<ToolkitApp> {\n if (!appInstance) {\n appInstance = await createApp(config)\n setApp(appInstance)\n }\n return appInstance\n}\n`,\n )\n\n // drizzle.config.ts\n await writeFile(\n join(projectDir, 'drizzle.config.ts'),\n `import type { Config } from 'drizzle-kit'\n\nif (!process.env.DATABASE_URL) {\n throw new Error('DATABASE_URL environment variable is required')\n}\n\nexport default {\n schema: ['./generated/schema.ts', './generated/auth-schema.ts'],\n out: './migrations',\n dialect: 'postgresql',\n dbCredentials: {\n url: process.env.DATABASE_URL,\n },\n} satisfies Config\n`,\n )\n\n // auth.config.ts\n await writeFile(\n join(projectDir, 'auth.config.ts'),\n `import { betterAuth } from 'better-auth'\nimport { drizzleAdapter } from 'better-auth/adapters/drizzle'\nimport { admin } from 'better-auth/plugins'\nimport { organization } from 'better-auth/plugins/organization'\nimport { drizzle } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\n\nconst sql = postgres(process.env.DATABASE_URL!)\nconst db = drizzle(sql)\n\nexport const auth = betterAuth({\n database: drizzleAdapter(db, { provider: 'pg' }),\n emailAndPassword: { enabled: true },\n plugins: [\n admin(),\n organization(),\n ],\n})\n`,\n )\n\n // generated/auth-schema.ts (placeholder)\n await writeFile(\n join(projectDir, 'generated/auth-schema.ts'),\n `// This file is generated by better-auth CLI.\n// Run: npx @better-auth/cli generate --config auth.config.ts -y\nexport {}\n`,\n )\n}\n","import { join } from 'node:path'\nimport { generateAdmin } from './generators/admin'\nimport { generateAuth } from './generators/auth'\nimport { generateBase } from './generators/base'\nimport { generateEntities } from './generators/entities'\nimport { generateIntl } from './generators/intl'\nimport { generateMulti } from './generators/multi'\nimport { generateNextjs } from './generators/nextjs'\nimport { generateScripts } from './generators/scripts'\nimport { generateTheme } from './generators/theme'\nimport { generateToolkit } from './generators/toolkit'\nimport type { ProjectOptions } from './types'\nimport { deleteFiles, mergePackageJson, runCommand } from './utils'\nimport { CREATE_NEXT_APP_VERSION, DEPS } from './versions'\n\nexport type ProgressCallback = (message: string) => void\n\nexport async function scaffold(\n options: ProjectOptions,\n onProgress?: ProgressCallback,\n): Promise<void> {\n if (options.mode === 'single') {\n await scaffoldSingle(options, onProgress)\n } else {\n await scaffoldMulti(options, onProgress)\n }\n}\n\nasync function scaffoldSingle(\n options: ProjectOptions,\n onProgress?: ProgressCallback,\n): Promise<void> {\n const projectDir = join(process.cwd(), options.name)\n\n // 1. Run create-next-app\n onProgress?.('Creating Next.js app...')\n runCommand(\n `pnpm create next-app@${CREATE_NEXT_APP_VERSION} ${options.name} ` +\n '--typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias \"@/*\"',\n )\n\n // 2. Clean up artifacts\n await deleteFiles(projectDir, ['app/page.tsx', 'app/page.module.css', 'app/fonts', 'README.md'])\n\n // 3. Merge package.json — ESM required for @murumets-ee/* packages\n await mergePackageJson(join(projectDir, 'package.json'), {\n type: 'module',\n dependencies: {\n '@murumets-ee/core': `^${DEPS.myorgCore}`,\n '@murumets-ee/db': `^${DEPS.myorgDb}`,\n '@murumets-ee/entity': `^${DEPS.myorgEntity}`,\n '@murumets-ee/logging': `^${DEPS.myorgLogging}`,\n '@murumets-ee/auth': `^${DEPS.myorgAuth}`,\n '@murumets-ee/auth-ui': `^${DEPS.myorgAuthUi}`,\n '@murumets-ee/admin-ui': `^${DEPS.myorgAdminUi}`,\n '@murumets-ee/content': `^${DEPS.myorgContent}`,\n '@murumets-ee/settings': `^${DEPS.myorgSettings}`,\n '@murumets-ee/storage': `^${DEPS.myorgStorage}`,\n '@murumets-ee/media': `^${DEPS.myorgMedia}`,\n '@murumets-ee/taxonomy': `^${DEPS.myorgTaxonomy}`,\n '@murumets-ee/queue': `^${DEPS.myorgQueue}`,\n '@murumets-ee/mail': `^${DEPS.myorgMail}`,\n '@murumets-ee/ticketing': `^${DEPS.myorgTicketing}`,\n '@murumets-ee/ticketing-ui': `^${DEPS.myorgTicketingUi}`,\n '@murumets-ee/tokens': `^${DEPS.myorgTokens}`,\n '@murumets-ee/editor': `^${DEPS.myorgEditor}`,\n '@murumets-ee/blocks': `^${DEPS.myorgBlocks}`,\n '@murumets-ee/ui': `^${DEPS.myorgUi}`,\n 'better-auth': `^${DEPS.betterAuth}`,\n 'drizzle-orm': `^${DEPS.drizzleOrm}`,\n 'next-intl': `^${DEPS.nextIntl}`,\n 'next-themes': `^${DEPS.nextThemes}`,\n 'lucide-react': `^${DEPS.lucideReact}`,\n postgres: `^${DEPS.postgres}`,\n '@tanstack/react-query': `^${DEPS.tanstackReactQuery}`,\n '@tanstack/react-table': `^${DEPS.tanstackReactTable}`,\n 'react-hook-form': `^${DEPS.reactHookForm}`,\n '@hookform/resolvers': `^${DEPS.hookformResolvers}`,\n zod: `^${DEPS.zod}`,\n },\n devDependencies: {\n 'drizzle-kit': `^${DEPS.drizzleKit}`,\n tsx: `^${DEPS.tsx}`,\n 'babel-plugin-react-compiler': DEPS.babelReactCompiler,\n },\n scripts: {\n 'db:generate': 'tsx --env-file=.env scripts/generate-schema.ts',\n 'db:migrate:generate': 'drizzle-kit generate',\n 'db:migrate': 'tsx --env-file=.env scripts/migrate.ts',\n 'db:reset': 'tsx --env-file=.env scripts/reset-db.ts',\n worker: 'tsx --env-file=.env scripts/worker.ts',\n },\n })\n\n // 4. Run all generators\n onProgress?.('Adding toolkit files...')\n await generateBase(projectDir, options)\n await generateEntities(projectDir, options)\n await generateToolkit(projectDir, options)\n await generateAuth(projectDir, options)\n await generateIntl(projectDir, options)\n await generateNextjs(projectDir, options)\n await generateTheme(projectDir, options)\n await generateAdmin(projectDir, options)\n await generateScripts(projectDir, options)\n\n // 5. Install deps\n if (options.installDeps) {\n onProgress?.('Installing dependencies...')\n runCommand('pnpm install', { cwd: projectDir })\n }\n}\n\nasync function scaffoldMulti(\n options: ProjectOptions,\n onProgress?: ProgressCallback,\n): Promise<void> {\n const projectDir = join(process.cwd(), options.name)\n\n // generateMulti handles everything: root workspace, create-next-app x2, overlays\n onProgress?.('Creating workspace...')\n await generateMulti(projectDir, options, onProgress)\n\n // Install deps\n if (options.installDeps) {\n onProgress?.('Installing dependencies...')\n runCommand('pnpm install', { cwd: projectDir })\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{dirname as e,join as t}from"node:path";import{execSync as n}from"node:child_process";import{appendFile as r,mkdir as i,readFile as a,rm as o,writeFile as s}from"node:fs/promises";var c=Object.defineProperty,l=((e,t)=>{let n={};for(var r in e)c(n,r,{get:e[r],enumerable:!0});return t||c(n,Symbol.toStringTag,{value:`Module`}),n})({appendToFile:()=>m,deleteFiles:()=>f,mergePackageJson:()=>d,runCommand:()=>p,writeFile:()=>u});async function u(t,n){await i(e(t),{recursive:!0}),await s(t,n,`utf-8`)}async function d(e,t){let n=await a(e,`utf-8`),r=JSON.parse(n);t.type&&(r.type=t.type),t.dependencies&&(r.dependencies={...r.dependencies,...t.dependencies}),t.devDependencies&&(r.devDependencies={...r.devDependencies,...t.devDependencies}),t.scripts&&(r.scripts={...r.scripts,...t.scripts}),await s(e,`${JSON.stringify(r,null,2)}\n`,`utf-8`)}async function f(e,n){for(let r of n)await o(t(e,r),{force:!0,recursive:!0})}function p(e,t){return n(e,{cwd:t?.cwd,stdio:`pipe`,encoding:`utf-8`})}async function m(e,t){await r(e,t,`utf-8`)}export{l as a,p as i,f as n,u as o,d as r,m as t};
2
+ //# sourceMappingURL=utils-Bmw75lEa.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils-Bmw75lEa.mjs","names":["writeFile","fsWriteFile"],"sources":["../src/utils.ts"],"sourcesContent":["import { execSync } from 'node:child_process'\nimport { appendFile, writeFile as fsWriteFile, mkdir, readFile, rm } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\n\n/** Write a file, creating parent directories as needed */\nexport async function writeFile(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true })\n await fsWriteFile(filePath, content, 'utf-8')\n}\n\n/** Read and parse package.json, merge in new deps/scripts, write back */\nexport async function mergePackageJson(\n pkgPath: string,\n merge: {\n type?: string\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n scripts?: Record<string, string>\n },\n): Promise<void> {\n const raw = await readFile(pkgPath, 'utf-8')\n const pkg = JSON.parse(raw)\n\n if (merge.type) {\n pkg.type = merge.type\n }\n if (merge.dependencies) {\n pkg.dependencies = { ...pkg.dependencies, ...merge.dependencies }\n }\n if (merge.devDependencies) {\n pkg.devDependencies = { ...pkg.devDependencies, ...merge.devDependencies }\n }\n if (merge.scripts) {\n pkg.scripts = { ...pkg.scripts, ...merge.scripts }\n }\n\n await fsWriteFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`, 'utf-8')\n}\n\n/** Delete files (silently ignores missing) */\nexport async function deleteFiles(basePath: string, files: string[]): Promise<void> {\n for (const file of files) {\n await rm(join(basePath, file), { force: true, recursive: true })\n }\n}\n\n/** Run a shell command synchronously */\nexport function runCommand(cmd: string, options?: { cwd?: string }): string {\n return execSync(cmd, {\n cwd: options?.cwd,\n stdio: 'pipe',\n encoding: 'utf-8',\n })\n}\n\n/** Append content to a file */\nexport async function appendToFile(filePath: string, content: string): Promise<void> {\n await appendFile(filePath, content, 'utf-8')\n}\n"],"mappings":"gbAKA,eAAsBA,EAAU,EAAkB,EAAgC,CAChF,MAAM,EAAM,EAAQ,EAAS,CAAE,CAAE,UAAW,GAAM,CAAC,CACnD,MAAMC,EAAY,EAAU,EAAS,QAAQ,CAI/C,eAAsB,EACpB,EACA,EAMe,CACf,IAAM,EAAM,MAAM,EAAS,EAAS,QAAQ,CACtC,EAAM,KAAK,MAAM,EAAI,CAEvB,EAAM,OACR,EAAI,KAAO,EAAM,MAEf,EAAM,eACR,EAAI,aAAe,CAAE,GAAG,EAAI,aAAc,GAAG,EAAM,aAAc,EAE/D,EAAM,kBACR,EAAI,gBAAkB,CAAE,GAAG,EAAI,gBAAiB,GAAG,EAAM,gBAAiB,EAExE,EAAM,UACR,EAAI,QAAU,CAAE,GAAG,EAAI,QAAS,GAAG,EAAM,QAAS,EAGpD,MAAMA,EAAY,EAAS,GAAG,KAAK,UAAU,EAAK,KAAM,EAAE,CAAC,IAAK,QAAQ,CAI1E,eAAsB,EAAY,EAAkB,EAAgC,CAClF,IAAK,IAAM,KAAQ,EACjB,MAAM,EAAG,EAAK,EAAU,EAAK,CAAE,CAAE,MAAO,GAAM,UAAW,GAAM,CAAC,CAKpE,SAAgB,EAAW,EAAa,EAAoC,CAC1E,OAAO,EAAS,EAAK,CACnB,IAAK,GAAS,IACd,MAAO,OACP,SAAU,QACX,CAAC,CAIJ,eAAsB,EAAa,EAAkB,EAAgC,CACnF,MAAM,EAAW,EAAU,EAAS,QAAQ"}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@murumets-ee/create",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "bin": {
7
- "create-lumi": "./dist/cli.js"
7
+ "create-lumi": "./dist/cli.mjs"
8
8
  },
9
9
  "exports": {
10
10
  ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
11
+ "types": "./dist/index.d.mts",
12
+ "import": "./dist/index.mjs"
13
13
  }
14
14
  },
15
15
  "files": [
@@ -20,12 +20,12 @@
20
20
  "picocolors": "^1.1.0"
21
21
  },
22
22
  "devDependencies": {
23
- "tsup": "^8.3.5",
23
+ "tsdown": "^0.21.7",
24
24
  "typescript": "^5.7.3"
25
25
  },
26
26
  "scripts": {
27
- "build": "tsup",
28
- "dev": "tsup --watch",
27
+ "build": "tsdown",
28
+ "dev": "tsdown --watch",
29
29
  "test": "vitest"
30
30
  }
31
31
  }
@@ -1,2 +0,0 @@
1
- import{execSync as r}from"child_process";import{appendFile as c,writeFile as s,mkdir as d,readFile as o,rm as p}from"fs/promises";import{dirname as a,join as f}from"path";async function v(i,e){await d(a(i),{recursive:!0}),await s(i,e,"utf-8")}async function l(i,e){let t=await o(i,"utf-8"),n=JSON.parse(t);e.type&&(n.type=e.type),e.dependencies&&(n.dependencies={...n.dependencies,...e.dependencies}),e.devDependencies&&(n.devDependencies={...n.devDependencies,...e.devDependencies}),e.scripts&&(n.scripts={...n.scripts,...e.scripts}),await s(i,`${JSON.stringify(n,null,2)}
2
- `,"utf-8")}async function y(i,e){for(let t of e)await p(f(i,t),{force:!0,recursive:!0})}function m(i,e){return r(i,{cwd:e?.cwd,stdio:"pipe",encoding:"utf-8"})}async function F(i,e){await c(i,e,"utf-8")}export{v as a,l as b,y as c,m as d,F as e};
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- import{execSync as r}from"child_process";import{appendFile as c,writeFile as s,mkdir as d,readFile as o,rm as p}from"fs/promises";import{dirname as a,join as f}from"path";async function v(i,e){await d(a(i),{recursive:!0}),await s(i,e,"utf-8")}async function l(i,e){let t=await o(i,"utf-8"),n=JSON.parse(t);e.type&&(n.type=e.type),e.dependencies&&(n.dependencies={...n.dependencies,...e.dependencies}),e.devDependencies&&(n.devDependencies={...n.devDependencies,...e.devDependencies}),e.scripts&&(n.scripts={...n.scripts,...e.scripts}),await s(i,`${JSON.stringify(n,null,2)}
3
- `,"utf-8")}async function y(i,e){for(let t of e)await p(f(i,t),{force:!0,recursive:!0})}function m(i,e){return r(i,{cwd:e?.cwd,stdio:"pipe",encoding:"utf-8"})}async function F(i,e){await c(i,e,"utf-8")}export{v as a,l as b,y as c,m as d,F as e};