@murumets-ee/create 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +49 -1168
- package/dist/index.d.mts +1 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +48 -1167
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/template-demo.tar.gz +0 -0
- package/dist/utils-DHetVfSn.mjs +0 -2
- package/dist/utils-DHetVfSn.mjs.map +0 -1
- package/templates/template-blank.tar.gz +0 -0
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["writeFile","tarExtract"],"sources":["../src/versions.ts","../src/generators/multi.ts","../src/scaffold.ts"],"sourcesContent":["export const TOOLKIT_VERSION = '0.2.0'\nexport const CREATE_NEXT_APP_VERSION = '16'\n\nexport const DEPS = {\n // toolkit packages — must match exactly what's on npmjs.org.\n // Bumps for Phase 4 (auto-admin + tarball scaffolder + playground migration):\n // admin-ui, ticketing-ui, ticketing, entity, content, media, create\n // Everything else stays at its previously-published version.\n myorgCore: '0.1.6',\n myorgDb: '0.1.5',\n myorgEntity: '0.2.0',\n myorgLogging: '0.1.6',\n myorgAuth: '0.1.5',\n myorgAuthUi: '0.1.5',\n myorgAdminUi: '0.2.0',\n myorgContent: '0.2.0',\n myorgContentApi: '0.1.5',\n myorgSettings: '0.1.6',\n myorgStorage: '0.1.6',\n myorgMedia: '0.2.0',\n myorgTaxonomy: '0.1.5',\n myorgQueue: '0.1.6',\n myorgMail: '0.1.5',\n myorgTicketing: '0.2.0',\n myorgTicketingUi: '0.2.0',\n myorgEditor: '0.1.5',\n myorgBlocks: '0.1.5',\n myorgTokens: '0.1.5',\n myorgUi: '0.1.5',\n myorgCli: '0.1.10',\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 runtime — frontend stack (must match playground exactly)\n next: '16.1.6',\n react: '19.2.3',\n reactDom: '19.2.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 { getAuth } from '@murumets-ee/auth'\nimport { getToolkitApp } from './app'\n\nawait getToolkitApp()\n\nexport const auth = getAuth()\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 { auth } from '@${name}/config/auth'\n\nconst { GET, POST } = toNextJsHandler(auth)\nexport { GET, POST }\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 →\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 { existsSync, readFileSync } from 'node:fs'\nimport { readdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { extract as tarExtract } from 'tar'\nimport { generateMulti } from './generators/multi'\nimport type { ProjectOptions, Template } from './types'\nimport { deleteFiles, runCommand } from './utils'\nimport { CREATE_NEXT_APP_VERSION } from './versions'\n\nexport type ProgressCallback = (message: string) => void\n\n// File extensions eligible for token replacement during scaffolding.\n// Mirrors the manifest's `TOKENIZABLE_EXTENSIONS` — kept inline so the\n// runtime CLI doesn't depend on the build-time `scripts/` directory.\nconst TOKENIZABLE_EXTENSIONS = new Set<string>([\n '.ts',\n '.tsx',\n '.mts',\n '.json',\n '.md',\n '.yml',\n '.yaml',\n '.css',\n '.html',\n])\n\nconst TOKENIZABLE_BASENAMES = new Set<string>(['.env.example', 'Dockerfile', '.gitignore'])\n\nconst TOKEN_PROJECT_NAME = '__PROJECT_NAME__'\nconst TOKEN_PROJECT_NAME_TITLE = '__PROJECT_NAME_TITLE__'\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\n// ---------------------------------------------------------------------------\n// scaffoldSingle — extract a prebuilt tarball, then tokenize.\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 to get an authoritative Next.js skeleton\n // (lockfile, ESLint config, default tsconfig, etc.)\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. Cleanup default Next.js artifacts that the template will replace\n await deleteFiles(projectDir, [\n 'app/page.tsx',\n 'app/page.module.css',\n 'app/fonts',\n 'app/globals.css',\n 'README.md',\n ])\n\n // 3. Extract the prebuilt template tarball over the project directory\n onProgress?.('Extracting template...')\n await extractTemplate(options.template, projectDir)\n\n // 4. Replace tokens in tokenizable files\n onProgress?.('Personalizing template...')\n await tokenizeProject(projectDir, options.name)\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 // Multi-mode is unchanged from the previous scaffolder; tarball-based\n // multi support will land in a follow-up.\n onProgress?.('Creating workspace...')\n await generateMulti(projectDir, options, onProgress)\n\n if (options.installDeps) {\n onProgress?.('Installing dependencies...')\n runCommand('pnpm install', { cwd: projectDir })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tarball extraction\n// ---------------------------------------------------------------------------\n\nasync function extractTemplate(template: Template, destDir: string): Promise<void> {\n const tarball = resolveTemplateTarball(template)\n if (!existsSync(tarball)) {\n throw new Error(\n `Template tarball missing: ${tarball}\\n` +\n `Run \\`pnpm --filter @murumets-ee/create build:templates\\` first.`,\n )\n }\n await tarExtract({ file: tarball, cwd: destDir })\n}\n\n/**\n * Locate the template tarball relative to the published `@murumets-ee/create`\n * package root.\n *\n * Anchors on the package's own `package.json` (matched by `name`) so a parent\n * project that happens to have a `templates/` directory at a higher path\n * can't hijack the resolution. This is a defense-in-depth measure against\n * template-substitution attacks where `create-lumi` is invoked from inside\n * a malicious project layout.\n *\n * In the published npm tarball, this file lives at `dist/cli.mjs` and the\n * templates live at `templates/*.tar.gz` next to `dist/`. Walking up from\n * `import.meta.url` should find the package root in 2-3 hops.\n */\nfunction resolveTemplateTarball(template: Template): string {\n const here = dirname(fileURLToPath(import.meta.url))\n let dir = here\n for (let i = 0; i < 5; i++) {\n const pkgJsonPath = join(dir, 'package.json')\n if (existsSync(pkgJsonPath)) {\n try {\n // Sync read is fine — runs once at scaffold time, not in a hot loop.\n const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string }\n if (pkg.name === '@murumets-ee/create') {\n return join(dir, 'templates', `template-${template}.tar.gz`)\n }\n } catch {\n // Malformed package.json — keep walking.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) break // hit filesystem root\n dir = parent\n }\n throw new Error(\n `Could not find @murumets-ee/create package root above ${here}. ` +\n `template-${template}.tar.gz cannot be located.`,\n )\n}\n\n// ---------------------------------------------------------------------------\n// Token replacement\n// ---------------------------------------------------------------------------\n\n/**\n * Walk the project directory and replace `__PROJECT_NAME__` /\n * `__PROJECT_NAME_TITLE__` tokens in eligible text files.\n *\n * Binary files (images, fonts) and `node_modules` / `.next` are skipped\n * to avoid corruption and pointless work.\n */\nasync function tokenizeProject(projectDir: string, projectName: string): Promise<void> {\n const titleName = toTitleCase(projectName)\n for await (const filePath of walkFiles(projectDir)) {\n if (!isTokenizable(filePath)) continue\n const original = await readFile(filePath, 'utf-8')\n if (!original.includes(TOKEN_PROJECT_NAME) && !original.includes(TOKEN_PROJECT_NAME_TITLE)) {\n continue\n }\n const replaced = original\n .split(TOKEN_PROJECT_NAME_TITLE)\n .join(titleName)\n .split(TOKEN_PROJECT_NAME)\n .join(projectName)\n await writeFile(filePath, replaced, 'utf-8')\n }\n}\n\n/**\n * Directories the tokenizer must skip.\n *\n * Pure deny-list — every other directory is walked. Hidden directories\n * (`.git`, `.next`, `.turbo`, etc.) are skipped wholesale by the\n * `name.startsWith('.')` check below; explicit names are listed for\n * common build outputs that don't start with a dot.\n */\nconst SKIP_DIRS = new Set<string>([\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'out',\n])\n\nasync function* walkFiles(root: string): AsyncGenerator<string> {\n const entries = await readdir(root, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(root, entry.name)\n if (entry.isDirectory()) {\n // Skip build/cache directories AND any hidden dir (.git, .next,\n // .turbo, .vscode, .idea, …). Hidden FILES like .env.example must\n // still be visited — that's why this check is dir-only.\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue\n yield* walkFiles(full)\n } else if (entry.isFile()) {\n yield full\n }\n }\n}\n\nfunction isTokenizable(filePath: string): boolean {\n const slash = filePath.lastIndexOf('/')\n const basename = slash >= 0 ? filePath.slice(slash + 1) : filePath\n if (TOKENIZABLE_BASENAMES.has(basename)) return true\n const dot = basename.lastIndexOf('.')\n if (dot < 0) return false\n const ext = basename.slice(dot)\n return TOKENIZABLE_EXTENSIONS.has(ext)\n}\n\nfunction toTitleCase(name: string): string {\n return name\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((part) => part[0]?.toUpperCase() + part.slice(1))\n .join(' ')\n}\n\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["tarExtract"],"sources":["../src/utils.ts","../src/scaffold.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","import { existsSync, readFileSync } from 'node:fs'\nimport { readdir, readFile, rm, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { extract as tarExtract } from 'tar'\nimport type { ProjectOptions } from './types'\nimport { deleteFiles, runCommand } from './utils'\nimport { CREATE_NEXT_APP_VERSION } from './versions'\n\nexport type ProgressCallback = (message: string) => void\n\n// File extensions eligible for token replacement during scaffolding.\nconst TOKENIZABLE_EXTENSIONS = new Set<string>([\n '.ts',\n '.tsx',\n '.mts',\n '.json',\n '.md',\n '.yml',\n '.yaml',\n '.css',\n '.html',\n])\n\nconst TOKENIZABLE_BASENAMES = new Set<string>(['.env.example', 'Dockerfile', '.gitignore'])\n\nconst TOKEN_PROJECT_NAME = '__PROJECT_NAME__'\nconst TOKEN_PROJECT_NAME_TITLE = '__PROJECT_NAME_TITLE__'\n\nexport async function scaffold(\n options: ProjectOptions,\n onProgress?: ProgressCallback,\n): Promise<void> {\n await scaffoldSingle(options, onProgress)\n}\n\n// ---------------------------------------------------------------------------\n// scaffoldSingle — extract a prebuilt tarball, then tokenize.\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 to get an authoritative Next.js skeleton\n // (lockfile, ESLint config, default tsconfig, etc.)\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. Cleanup default Next.js artifacts that the template will replace\n await deleteFiles(projectDir, [\n 'app/page.tsx',\n 'app/page.module.css',\n 'app/fonts',\n 'app/globals.css',\n 'README.md',\n ])\n\n // 3. Extract the prebuilt template tarball over the project directory.\n // Both demo and blank use the same tarball — blank strips content after.\n onProgress?.('Extracting template...')\n await extractTemplate(projectDir)\n\n // 4. For blank template: remove demo entities, seeds, and demo routes\n if (options.template === 'blank') {\n onProgress?.('Stripping demo content for blank template...')\n await stripForBlank(projectDir)\n }\n\n // 5. Replace tokens in tokenizable files\n onProgress?.('Personalizing template...')\n await tokenizeProject(projectDir, options.name)\n\n // 6. Install deps\n if (options.installDeps) {\n onProgress?.('Installing dependencies...')\n runCommand('pnpm install', { cwd: projectDir })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tarball extraction\n// ---------------------------------------------------------------------------\n\nasync function extractTemplate(destDir: string): Promise<void> {\n const tarball = resolveTemplateTarball()\n if (!existsSync(tarball)) {\n throw new Error(\n `Template tarball missing: ${tarball}\\n` +\n `Run \\`pnpm --filter @murumets-ee/create build:templates\\` first.`,\n )\n }\n await tarExtract({ file: tarball, cwd: destDir })\n}\n\n/**\n * Locate the template tarball relative to the published `@murumets-ee/create`\n * package root.\n *\n * Anchors on the package's own `package.json` (matched by `name`) so a parent\n * project that happens to have a `templates/` directory at a higher path\n * can't hijack the resolution. This is a defense-in-depth measure against\n * template-substitution attacks where `create-lumi` is invoked from inside\n * a malicious project layout.\n */\nfunction resolveTemplateTarball(): string {\n const here = dirname(fileURLToPath(import.meta.url))\n let dir = here\n for (let i = 0; i < 5; i++) {\n const pkgJsonPath = join(dir, 'package.json')\n if (existsSync(pkgJsonPath)) {\n try {\n const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string }\n if (pkg.name === '@murumets-ee/create') {\n return join(dir, 'templates', 'template-demo.tar.gz')\n }\n } catch {\n // Malformed package.json — keep walking.\n }\n }\n const parent = dirname(dir)\n if (parent === dir) break // hit filesystem root\n dir = parent\n }\n throw new Error(\n `Could not find @murumets-ee/create package root above ${here}. ` +\n `template-demo.tar.gz cannot be located.`,\n )\n}\n\n// ---------------------------------------------------------------------------\n// Blank template: post-extract cleanup\n// ---------------------------------------------------------------------------\n\n/** admin-config.ts for blank template — plugin entities only, no project entities. */\nconst BLANK_ADMIN_CONFIG = `/**\n * Shared admin configuration — entities + route resource metadata.\n *\n * Add your own entities to the arrays below after defining them in entities/.\n */\n\nimport { Media } from '@murumets-ee/media'\nimport {\n Ticket,\n TicketMessage,\n TicketAttachment,\n Department,\n TicketTag,\n} from '@murumets-ee/ticketing'\n\n/** All content entities registered in the admin API. */\nexport const allEntities = [\n Media,\n Ticket, TicketMessage, TicketAttachment, Department, TicketTag,\n]\n\n/** Taxonomy vocabularies. */\nexport const taxonomyVocabularies = {}\n\n/** Entities exposed via generic CRUD handler (subset of allEntities). */\nexport const crudEntities = [\n Media,\n Ticket, TicketMessage, TicketAttachment,\n]\n\n/**\n * Plugin route resource declarations.\n */\nexport const pluginResources: { resource: string; actions: readonly string[] }[] = [\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 { resource: 'ticketing', actions: ['view', 'create', 'update', 'delete'] },\n]\n`\n\n/**\n * Strip demo content from a freshly extracted template to produce the\n * \"blank\" variant:\n *\n * 1. Delete entity files (keep `entities/index.ts` with empty exports)\n * 2. Delete seed files (keep `seeds/index.ts` with empty array)\n * 3. Delete `app/[locale]/(shell)/demos/` entirely\n * 4. Remove the demosGroup from `app/admin-layout.tsx`\n * 5. Rewrite toolkit.config.ts, admin-config.ts, content API route,\n * and admin dashboard to not reference project entities\n */\nasync function stripForBlank(projectDir: string): Promise<void> {\n // 1. Entities — delete all .ts files except index.ts, rewrite index to empty\n const entitiesDir = join(projectDir, 'entities')\n if (existsSync(entitiesDir)) {\n const files = await readdir(entitiesDir)\n for (const f of files) {\n if (f !== 'index.ts') await rm(join(entitiesDir, f), { force: true })\n }\n await writeFile(\n join(entitiesDir, 'index.ts'),\n '// Add your entities here and export them.\\n' +\n '// See https://github.com/murumets-ee/lumi-cms for documentation.\\n' +\n '\\n' +\n 'export const projectEntities = [] as const\\n' +\n 'export const projectTaxonomies = [] as const\\n',\n 'utf-8',\n )\n }\n\n // 2. Seeds — delete all .ts files except index.ts, rewrite index to empty\n const seedsDir = join(projectDir, 'seeds')\n if (existsSync(seedsDir)) {\n const files = await readdir(seedsDir)\n for (const f of files) {\n if (f !== 'index.ts') await rm(join(seedsDir, f), { force: true })\n }\n await writeFile(\n join(seedsDir, 'index.ts'),\n \"import type { AdminSeeder } from '@murumets-ee/admin-ui/pages'\\n\\n\" +\n 'export const projectSeeders: AdminSeeder[] = []\\n',\n 'utf-8',\n )\n }\n\n // 3. Demo routes\n const demosDir = join(projectDir, 'app', '[locale]', '(shell)', 'demos')\n await rm(demosDir, { recursive: true, force: true })\n\n // 4. Remove demosGroup from admin-layout.tsx (case-insensitive — comment says \"demo\")\n const layoutPath = join(projectDir, 'app', 'admin-layout.tsx')\n if (existsSync(layoutPath)) {\n let content = await readFile(layoutPath, 'utf-8')\n content = content.replace(\n /\\n\\s*\\/\\/.*demo.*[\\s\\S]*?const demosGroup[\\s\\S]*?\\}\\n/i,\n '\\n',\n )\n content = content.replace(', demosGroup', '')\n // Remove icon imports only used by demosGroup\n content = content.replace(/\\s*GalleryHorizontalEnd,\\n/, '\\n')\n content = content.replace(/\\s*HardDrive,\\n/, '\\n')\n content = content.replace(/\\s*Palette,\\n/, '\\n')\n await writeFile(layoutPath, content, 'utf-8')\n }\n\n // 5. Rewrite toolkit.config.ts — use projectEntities instead of named imports\n const configPath = join(projectDir, 'toolkit.config.ts')\n if (existsSync(configPath)) {\n let content = await readFile(configPath, 'utf-8')\n content = content.replace(\n /import \\{[^}]+\\} from '\\.\\/entities'/,\n \"import { projectEntities } from './entities'\",\n )\n content = content.replace(\n /entities: \\[[^\\]]+\\]/,\n 'entities: [...projectEntities]',\n )\n await writeFile(configPath, content, 'utf-8')\n }\n\n // 6. Rewrite lib/admin-config.ts — no project entities, only plugin entities\n const adminConfigPath = join(projectDir, 'lib', 'admin-config.ts')\n if (existsSync(adminConfigPath)) {\n await writeFile(\n adminConfigPath,\n BLANK_ADMIN_CONFIG,\n 'utf-8',\n )\n }\n\n // 7. Rewrite content API route — empty entity whitelist\n const contentRoutePath = join(projectDir, 'app', 'api', 'content', '[...path]', 'route.ts')\n if (existsSync(contentRoutePath)) {\n let content = await readFile(contentRoutePath, 'utf-8')\n content = content.replace(/import \\{[^}]+\\} from '@\\/entities'\\n/, '')\n content = content.replace(/entities: \\[[^\\]]+\\]/, 'entities: []')\n await writeFile(contentRoutePath, content, 'utf-8')\n }\n\n // 8. Replace admin dashboard page with auto-admin DashboardPage\n const dashboardPath = join(projectDir, 'app', '[locale]', '(shell)', 'admin', 'page.tsx')\n if (existsSync(dashboardPath)) {\n await writeFile(\n dashboardPath,\n \"import { pages } from '@/lib/admin-pages'\\n\\nexport default pages.DashboardPage\\n\",\n 'utf-8',\n )\n }\n}\n\n// ---------------------------------------------------------------------------\n// Token replacement\n// ---------------------------------------------------------------------------\n\n/**\n * Walk the project directory and replace `__PROJECT_NAME__` /\n * `__PROJECT_NAME_TITLE__` tokens in eligible text files.\n *\n * Binary files (images, fonts) and `node_modules` / `.next` are skipped\n * to avoid corruption and pointless work.\n */\nasync function tokenizeProject(projectDir: string, projectName: string): Promise<void> {\n const titleName = toTitleCase(projectName)\n for await (const filePath of walkFiles(projectDir)) {\n if (!isTokenizable(filePath)) continue\n const original = await readFile(filePath, 'utf-8')\n if (!original.includes(TOKEN_PROJECT_NAME) && !original.includes(TOKEN_PROJECT_NAME_TITLE)) {\n continue\n }\n const replaced = original\n .split(TOKEN_PROJECT_NAME_TITLE)\n .join(titleName)\n .split(TOKEN_PROJECT_NAME)\n .join(projectName)\n await writeFile(filePath, replaced, 'utf-8')\n }\n}\n\n/**\n * Directories the tokenizer must skip.\n */\nconst SKIP_DIRS = new Set<string>([\n 'node_modules',\n 'dist',\n 'build',\n 'coverage',\n 'out',\n])\n\nasync function* walkFiles(root: string): AsyncGenerator<string> {\n const entries = await readdir(root, { withFileTypes: true })\n for (const entry of entries) {\n const full = join(root, entry.name)\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue\n yield* walkFiles(full)\n } else if (entry.isFile()) {\n yield full\n }\n }\n}\n\nfunction isTokenizable(filePath: string): boolean {\n const slash = filePath.lastIndexOf('/')\n const basename = slash >= 0 ? filePath.slice(slash + 1) : filePath\n if (TOKENIZABLE_BASENAMES.has(basename)) return true\n const dot = basename.lastIndexOf('.')\n if (dot < 0) return false\n const ext = basename.slice(dot)\n return TOKENIZABLE_EXTENSIONS.has(ext)\n}\n\nfunction toTitleCase(name: string): string {\n return name\n .split(/[-_\\s]+/)\n .filter(Boolean)\n .map((part) => part[0]?.toUpperCase() + part.slice(1))\n .join(' ')\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
Binary file
|
package/dist/utils-DHetVfSn.mjs
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{appendFile as e,mkdir as t,readFile as n,rm as r,writeFile as i}from"node:fs/promises";import{dirname as a,join as o}from"node:path";import{execSync as s}from"node:child_process";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(e,n){await t(a(e),{recursive:!0}),await i(e,n,`utf-8`)}async function d(e,t){let r=await n(e,`utf-8`),a=JSON.parse(r);t.type&&(a.type=t.type),t.dependencies&&(a.dependencies={...a.dependencies,...t.dependencies}),t.devDependencies&&(a.devDependencies={...a.devDependencies,...t.devDependencies}),t.scripts&&(a.scripts={...a.scripts,...t.scripts}),await i(e,`${JSON.stringify(a,null,2)}\n`,`utf-8`)}async function f(e,t){for(let n of t)await r(o(e,n),{force:!0,recursive:!0})}function p(e,t){return s(e,{cwd:t?.cwd,stdio:`pipe`,encoding:`utf-8`})}async function m(t,n){await e(t,n,`utf-8`)}export{u as a,l as i,d as n,p as r,f as t};
|
|
2
|
-
//# sourceMappingURL=utils-DHetVfSn.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils-DHetVfSn.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"}
|
|
Binary file
|