@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 CHANGED
@@ -1,1177 +1,58 @@
1
1
  #!/usr/bin/env node
2
- import*as e from"@clack/prompts";import t from"picocolors";import{existsSync as n,readFileSync as r}from"node:fs";import{appendFile as i,mkdir as a,readFile as o,readdir as s,rm as c,writeFile as l}from"node:fs/promises";import{dirname as u,join as d}from"node:path";import{fileURLToPath as f}from"node:url";import{extract as p}from"tar";import{execSync as m}from"node:child_process";var h=Object.defineProperty,g=(e,t)=>{let n={};for(var r in e)h(n,r,{get:e[r],enumerable:!0});return t||h(n,Symbol.toStringTag,{value:`Module`}),n};function _(e){let t={},n=e.slice(2);for(let e=0;e<n.length;e++){let r=n[e];if(r===`--mode`&&n[e+1]){let r=n[++e];(r===`single`||r===`multi`)&&(t.mode=r)}else if(r===`--template`&&n[e+1]){let r=n[++e];(r===`demo`||r===`blank`)&&(t.template=r)}else r===`--install`?t.install=!0:r===`--no-install`?t.install=!1:!r.startsWith(`-`)&&!t.name&&(t.name=r)}return t}async function v(t){let n=_(process.argv),r=n.name??t;if(r&&n.mode&&n.template&&n.install!==void 0)return{name:r,mode:n.mode,template:n.template,installDeps:n.install};e.intro(`@murumets-ee/create`);let i=r??await e.text({message:`Project name:`,placeholder:`my-project`,validate:e=>{if(!e)return`Project name is required`;if(!/^[a-z0-9-]+$/.test(e))return`Use lowercase letters, numbers, and hyphens only`}});if(e.isCancel(i))return e.cancel(`Cancelled.`),null;let a=n.mode??await e.select({message:`App setup:`,options:[{value:`single`,label:`Single app`,hint:`everything in one Next.js project`},{value:`multi`,label:`Admin + Frontend`,hint:`two apps in a pnpm workspace`}]});if(e.isCancel(a))return e.cancel(`Cancelled.`),null;let o=n.template??await e.select({message:`Template:`,options:[{value:`demo`,label:`Demo`,hint:`sample entities, seeders, and content`},{value:`blank`,label:`Blank`,hint:`minimal scaffold — bring your own entities`}],initialValue:`demo`});if(e.isCancel(o))return e.cancel(`Cancelled.`),null;let s=n.install??await e.confirm({message:`Install dependencies?`,initialValue:!0});return e.isCancel(s)?(e.cancel(`Cancelled.`),null):{name:i,mode:a,template:o,installDeps:s}}var y=g({appendToFile:()=>w,deleteFiles:()=>S,mergePackageJson:()=>x,runCommand:()=>C,writeFile:()=>b});async function b(e,t){await a(u(e),{recursive:!0}),await l(e,t,`utf-8`)}async function x(e,t){let n=await o(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 l(e,`${JSON.stringify(r,null,2)}\n`,`utf-8`)}async function S(e,t){for(let n of t)await c(d(e,n),{force:!0,recursive:!0})}function C(e,t){return m(e,{cwd:t?.cwd,stdio:`pipe`,encoding:`utf-8`})}async function w(e,t){await i(e,t,`utf-8`)}const T={myorgCore:`0.1.6`,myorgDb:`0.1.5`,myorgEntity:`0.2.0`,myorgLogging:`0.1.6`,myorgAuth:`0.1.5`,myorgAuthUi:`0.1.5`,myorgAdminUi:`0.2.0`,myorgContent:`0.2.0`,myorgContentApi:`0.1.5`,myorgSettings:`0.1.6`,myorgStorage:`0.1.6`,myorgMedia:`0.2.0`,myorgTaxonomy:`0.1.5`,myorgQueue:`0.1.6`,myorgMail:`0.1.5`,myorgTicketing:`0.2.0`,myorgTicketingUi:`0.2.0`,myorgEditor:`0.1.5`,myorgBlocks:`0.1.5`,myorgTokens:`0.1.5`,myorgUi:`0.1.5`,myorgCli:`0.1.10`,betterAuth:`1.4.0`,drizzleOrm:`0.45.1`,nextIntl:`4.8.2`,nextThemes:`0.4.6`,lucideReact:`0.563.0`,postgres:`3.4.5`,reactHookForm:`7.71.1`,hookformResolvers:`5.2.2`,zod:`3.24.1`,tanstackReactQuery:`5.60.5`,tanstackReactTable:`8.21.3`,next:`16.1.6`,react:`19.2.3`,reactDom:`19.2.3`,drizzleKit:`0.31.10`,tsx:`4.19.2`,babelReactCompiler:`1.0.0`};async function E(e,t,n){await D(e,t),n?.(`Workspace root created`),n?.(`Creating admin app...`),await k(e,t),n?.(`Admin app created`),n?.(`Creating web app...`),await A(e,t),n?.(`Web app created`),await O(e,t),n?.(`Shared config package created`)}async function D(e,t){let{name:n}=t;await a(e,{recursive:!0}),await b(d(e,`pnpm-workspace.yaml`),`packages:
3
- - 'packages/*'
4
- - 'apps/*'
5
-
6
- ignoredBuiltDependencies:
7
- - sharp
8
- - unrs-resolver
9
- `),await b(d(e,`package.json`),`${JSON.stringify({name:`@${n}/monorepo`,version:`0.0.0`,private:!0,type:`module`,scripts:{dev:`turbo dev`,build:`turbo build`,"db:generate":`tsx --env-file=.env packages/config/scripts/generate-schema.ts`,"db:migrate:generate":`drizzle-kit generate --config packages/config/drizzle.config.ts`,"db:migrate":`tsx --env-file=.env packages/config/scripts/migrate.ts`,"db:reset":`tsx --env-file=.env packages/config/scripts/reset-db.ts`},devDependencies:{turbo:`^2.3.3`,typescript:`^5.7.3`,tsx:`^${T.tsx}`,"drizzle-kit":`^${T.drizzleKit}`}},null,2)}\n`),await b(d(e,`.gitignore`),`# dependencies
10
- node_modules/
11
-
12
- # build
13
- dist/
14
- .next/
15
- .turbo/
16
-
17
- # env
18
- .env*
19
- !.env.example
20
-
21
- # toolkit generated
22
- generated/
23
-
24
- # IDE
25
- .vscode/
26
- .idea/
27
-
28
- # OS
29
- .DS_Store
30
- Thumbs.db
31
-
32
- # logs
33
- *.log
34
-
35
- # migrations meta
36
- migrations/
37
-
38
- # turbo
39
- .turbo/
40
- `),await b(d(e,`.env.example`),`DATABASE_URL=postgresql://${n}:${n}_dev_password@localhost:5432/${n}_dev
41
- BETTER_AUTH_SECRET=dev-secret-change-me-in-production-min-32-chars
42
- BETTER_AUTH_URL=http://localhost:3000
43
- LOG_LEVEL=debug
44
- NEXT_PUBLIC_APP_URL=http://localhost:3000
45
- QUEUE_WORKER=true
46
- RESEND_API_KEY=
47
- RESEND_WEBHOOK_SECRET=
48
- MAIL_FROM=noreply@example.com
49
- CSAT_SECRET=
50
- `),await b(d(e,`docker-compose.yml`),`services:
51
- postgres:
52
- image: postgres:17-alpine
53
- container_name: ${n}-postgres
54
- restart: unless-stopped
55
- environment:
56
- POSTGRES_USER: ${n}
57
- POSTGRES_PASSWORD: ${n}_dev_password
58
- POSTGRES_DB: ${n}_dev
59
- ports:
60
- - "5432:5432"
61
- volumes:
62
- - postgres_data:/var/lib/postgresql/data
63
- healthcheck:
64
- test: ["CMD-SHELL", "pg_isready -U ${n} -d ${n}_dev"]
65
- interval: 10s
66
- timeout: 5s
67
- retries: 5
68
-
69
- volumes:
70
- postgres_data:
71
- name: ${n}_postgres_data
72
- `)}async function O(e,t){let{name:n}=t,r=d(e,`packages/config`);await b(d(r,`package.json`),`${JSON.stringify({name:`@${n}/config`,version:`0.1.0`,private:!0,type:`module`,exports:{".":`./toolkit.config.ts`,"./entities":`./entities/index.ts`,"./entities/*":`./entities/*`,"./app":`./lib/app.ts`,"./auth":`./lib/auth.ts`,"./auth-client":`./lib/auth-client.ts`},dependencies:{"@murumets-ee/core":`^${T.myorgCore}`,"@murumets-ee/db":`^${T.myorgDb}`,"@murumets-ee/entity":`^${T.myorgEntity}`,"@murumets-ee/logging":`^${T.myorgLogging}`,"@murumets-ee/auth":`^${T.myorgAuth}`,"better-auth":`^${T.betterAuth}`,"drizzle-orm":`^${T.drizzleOrm}`,postgres:`^${T.postgres}`,zod:`^${T.zod}`}},null,2)}\n`),await b(d(r,`tsconfig.json`),`${JSON.stringify({compilerOptions:{target:`ES2022`,module:`ESNext`,moduleResolution:`Bundler`,strict:!0,esModuleInterop:!0,skipLibCheck:!0,resolveJsonModule:!0},include:[`**/*.ts`]},null,2)}\n`),await b(d(r,`toolkit.config.ts`),`import { auth } from '@murumets-ee/auth/plugin'
73
- import { content } from '@murumets-ee/content/plugin'
74
- import { defineConfig } from '@murumets-ee/core'
75
- import { logging } from '@murumets-ee/logging/plugin'
76
- import { mail, ResendMailProvider } from '@murumets-ee/mail'
77
- import { media } from '@murumets-ee/media/plugin'
78
- import { queue } from '@murumets-ee/queue/plugin'
79
- import { settings } from '@murumets-ee/settings/plugin'
80
- import { storage } from '@murumets-ee/storage/plugin'
81
- import { taxonomy } from '@murumets-ee/taxonomy/plugin'
82
- import { ticketing } from '@murumets-ee/ticketing/plugin'
83
- import { Article, Category } from './entities'
84
- import * as authSchema from './generated/auth-schema'
85
-
86
- if (!process.env.DATABASE_URL) {
87
- throw new Error('DATABASE_URL environment variable is required')
88
- }
89
-
90
- export default defineConfig({
91
- db: {
92
- url: process.env.DATABASE_URL,
93
- poolMin: 2,
94
- poolMax: 10,
95
- },
96
- logging: {
97
- level: (process.env.LOG_LEVEL || 'info') as 'debug' | 'info' | 'warn' | 'error',
98
- name: '${n}',
99
- },
100
- entities: [Category, Article],
101
- plugins: [
102
- auth({ providers: ['email'], schema: authSchema }),
103
- content({
104
- locales: [{ code: 'en', label: 'English' }],
105
- defaultLocale: 'en',
106
- }),
107
- logging(),
108
- settings(),
109
- storage(),
110
- media(),
111
- taxonomy(),
112
- queue(),
113
- mail({
114
- provider: process.env.RESEND_API_KEY
115
- ? new ResendMailProvider({ apiKey: process.env.RESEND_API_KEY })
116
- : undefined,
117
- defaultFrom: process.env.MAIL_FROM ?? 'noreply@example.com',
118
- webhookSecret: process.env.RESEND_WEBHOOK_SECRET,
119
- }),
120
- ticketing({
121
- csatSecret: process.env.CSAT_SECRET,
122
- }),
123
- ],
124
- projectRoot: import.meta.dirname,
125
- })
126
- `),await b(d(r,`auth.config.ts`),`import { betterAuth } from 'better-auth'
127
- import { drizzleAdapter } from 'better-auth/adapters/drizzle'
128
- import { admin } from 'better-auth/plugins'
129
- import { organization } from 'better-auth/plugins/organization'
130
- import { drizzle } from 'drizzle-orm/postgres-js'
131
- import postgres from 'postgres'
132
-
133
- const sql = postgres(process.env.DATABASE_URL!)
134
- const db = drizzle(sql)
135
-
136
- export const auth = betterAuth({
137
- database: drizzleAdapter(db, { provider: 'pg' }),
138
- emailAndPassword: { enabled: true },
139
- plugins: [
140
- admin(),
141
- organization(),
142
- ],
143
- })
144
- `),await b(d(r,`drizzle.config.ts`),`import type { Config } from 'drizzle-kit'
145
-
146
- if (!process.env.DATABASE_URL) {
147
- throw new Error('DATABASE_URL environment variable is required')
148
- }
149
-
150
- export default {
151
- schema: ['./generated/schema.ts', './generated/auth-schema.ts'],
152
- out: './migrations',
153
- dialect: 'postgresql',
154
- dbCredentials: {
155
- url: process.env.DATABASE_URL,
156
- },
157
- } satisfies Config
158
- `),await b(d(r,`entities/index.ts`),`export { Article } from './article'
159
- export { Category } from './category'
160
- `),await b(d(r,`entities/article.ts`),`import { behavior, defineEntity, field } from '@murumets-ee/entity'
161
-
162
- export const Article = defineEntity({
163
- name: 'article',
164
- fields: {
165
- title: field.text({ required: true, maxLength: 200, indexed: true, translatable: true }),
166
- slug: field.slug({ from: 'title', unique: true }),
167
- excerpt: field.text({ maxLength: 500, translatable: true }),
168
- body: field.richtext(),
169
- viewCount: field.number({ default: 0, integer: true }),
170
- featured: field.boolean({ default: false }),
171
- publishDate: field.date(),
172
- contentType: field.select({ options: ['news', 'tutorial', 'announcement'], default: 'news' }),
173
- category: field.reference({ entity: 'category', required: false }),
174
- tags: field.reference({ entity: 'category', cardinality: 'many' }),
175
- coverImage: field.media({ accept: ['image/*'] }),
176
- },
177
- behaviors: [
178
- behavior.publishable(),
179
- behavior.auditable(),
180
- behavior.sluggable('title'),
181
- behavior.revisionable(),
182
- ],
183
- scope: 'global',
184
- access: {
185
- view: 'public',
186
- create: 'group.editor',
187
- update: 'group.editor',
188
- delete: 'group.admin',
189
- },
190
- })
191
- `),await b(d(r,`entities/category.ts`),`import { defineEntity, field } from '@murumets-ee/entity'
192
-
193
- export const Category = defineEntity({
194
- name: 'category',
195
- fields: {
196
- name: field.text({ required: true, maxLength: 100, indexed: true, translatable: true }),
197
- slug: field.slug({ from: 'name', unique: true }),
198
- description: field.text({ maxLength: 500, translatable: true }),
199
- },
200
- scope: 'global',
201
- access: {
202
- view: 'public',
203
- create: 'group.editor',
204
- update: 'group.editor',
205
- delete: 'group.admin',
206
- },
207
- })
208
- `),await b(d(r,`lib/app.ts`),`import { createApp, setApp, type ToolkitApp } from '@murumets-ee/core'
209
- import config from '../toolkit.config'
210
-
211
- let appInstance: ToolkitApp | null = null
212
-
213
- export async function getToolkitApp(): Promise<ToolkitApp> {
214
- if (!appInstance) {
215
- appInstance = await createApp(config)
216
- setApp(appInstance)
217
- }
218
- return appInstance
219
- }
220
- `),await b(d(r,`lib/auth.ts`),`import { getAuth } from '@murumets-ee/auth'
221
- import { getToolkitApp } from './app'
222
-
223
- await getToolkitApp()
224
-
225
- export const auth = getAuth()
226
- `),await b(d(r,`lib/auth-client.ts`),`import { createClient } from '@murumets-ee/auth/client'
227
-
228
- export const authClient = createClient()
229
- `),await b(d(r,`generated/auth-schema.ts`),`// This file is generated by better-auth CLI.
230
- // Run: npx @better-auth/cli generate --config auth.config.ts -y
231
- export {}
232
- `),await b(d(r,`scripts/generate-schema.ts`),`/**
233
- * Generate Drizzle schemas from entity definitions
234
- */
235
-
236
- import { mkdir, writeFile } from 'node:fs/promises'
237
- import { join } from 'node:path'
238
- import { generateSchemaCode, generateTranslationSchemaCode } from '@murumets-ee/entity'
239
- import config from '../toolkit.config'
240
-
241
- async function generateSchemas() {
242
- console.log('Generating Drizzle schemas from entity definitions...')
243
-
244
- const schemaDir = join(import.meta.dirname, '..', 'generated')
245
- await mkdir(schemaDir, { recursive: true })
246
-
247
- const imports: string[] = []
248
- const schemas: string[] = []
249
- let hasTranslations = false
250
-
251
- for (const entity of config.entities) {
252
- console.log(\` - Generating schema for \${entity.name}\`)
253
-
254
- const schemaCode = generateSchemaCode(entity)
255
- schemas.push(\`\\n// \${entity.name} table\`)
256
- schemas.push(schemaCode)
257
-
258
- const translationCode = generateTranslationSchemaCode(entity)
259
- if (translationCode) {
260
- hasTranslations = true
261
- schemas.push(\`\\n// \${entity.name} translations\`)
262
- schemas.push(translationCode)
263
- }
264
- }
265
-
266
- const pgCoreTypes = ['pgTable', 'varchar', 'text', 'boolean', 'timestamp', 'integer', 'doublePrecision', 'jsonb', 'uuid', 'index']
267
- if (hasTranslations) pgCoreTypes.push('unique')
268
- const pgCoreImports = \`import { \${pgCoreTypes.join(', ')} } from 'drizzle-orm/pg-core'\\n\`
269
- imports.push(pgCoreImports)
270
-
271
- const schemaFile = join(schemaDir, 'schema.ts')
272
- await writeFile(schemaFile, [...imports, ...schemas].join('\\n'))
273
-
274
- console.log(\`\\nSchemas written to: \${schemaFile}\`)
275
- console.log('Run \\\`pnpm db:migrate:generate\\\` to create migrations')
276
- }
277
-
278
- generateSchemas().catch(console.error)
279
- `),await b(d(r,`scripts/migrate.ts`),`/**
280
- * Run pending migrations
281
- */
282
-
283
- import { createDbClient, runMigrations } from '@murumets-ee/db'
284
-
285
- async function migrate() {
286
- console.log('Running migrations...')
287
-
288
- try {
289
- if (!process.env.DATABASE_URL) {
290
- throw new Error('DATABASE_URL environment variable is required')
291
- }
292
-
293
- const db = createDbClient({ url: process.env.DATABASE_URL })
294
- await runMigrations(db, import.meta.dirname + '/..')
295
-
296
- console.log('Migrations completed successfully')
297
- process.exit(0)
298
- } catch (error) {
299
- console.error('Migration failed:', error)
300
- process.exit(1)
301
- }
302
- }
303
-
304
- migrate()
305
- `),await b(d(r,`scripts/reset-db.ts`),`/**
306
- * Reset database (DROP all tables)
307
- * WARNING: Only for development
2
+ import*as e from"@clack/prompts";import t from"picocolors";import{existsSync as n,readFileSync as r}from"node:fs";import{readFile as i,readdir as a,rm as o,writeFile as s}from"node:fs/promises";import{dirname as c,join as l}from"node:path";import{fileURLToPath as u}from"node:url";import{extract as d}from"tar";import{execSync as f}from"node:child_process";function p(e){let t={},n=e.slice(2);for(let e=0;e<n.length;e++){let r=n[e];if(r===`--template`&&n[e+1]){let r=n[++e];(r===`demo`||r===`blank`)&&(t.template=r)}else r===`--install`?t.install=!0:r===`--no-install`?t.install=!1:!r.startsWith(`-`)&&!t.name&&(t.name=r)}return t}async function m(t){let n=p(process.argv),r=n.name??t;if(r&&n.template&&n.install!==void 0)return{name:r,template:n.template,installDeps:n.install};e.intro(`@murumets-ee/create`);let i=r??await e.text({message:`Project name:`,placeholder:`my-project`,validate:e=>{if(!e)return`Project name is required`;if(!/^[a-z0-9-]+$/.test(e))return`Use lowercase letters, numbers, and hyphens only`}});if(e.isCancel(i))return e.cancel(`Cancelled.`),null;let a=n.template??await e.select({message:`Template:`,options:[{value:`demo`,label:`Demo`,hint:`sample entities, seeders, and content`},{value:`blank`,label:`Blank`,hint:`minimal scaffold — bring your own entities`}],initialValue:`demo`});if(e.isCancel(a))return e.cancel(`Cancelled.`),null;let o=n.install??await e.confirm({message:`Install dependencies?`,initialValue:!0});return e.isCancel(o)?(e.cancel(`Cancelled.`),null):{name:i,template:a,installDeps:o}}async function h(e,t){for(let n of t)await o(l(e,n),{force:!0,recursive:!0})}function g(e,t){return f(e,{cwd:t?.cwd,stdio:`pipe`,encoding:`utf-8`})}const _=new Set([`.ts`,`.tsx`,`.mts`,`.json`,`.md`,`.yml`,`.yaml`,`.css`,`.html`]),v=new Set([`.env.example`,`Dockerfile`,`.gitignore`]),y=`__PROJECT_NAME__`,b=`__PROJECT_NAME_TITLE__`;async function x(e,t){await S(e,t)}async function S(e,t){let n=l(process.cwd(),e.name);t?.(`Creating Next.js app...`),g(`pnpm create next-app@16 ${e.name} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await h(n,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`app/globals.css`,`README.md`]),t?.(`Extracting template...`),await C(n),e.template===`blank`&&(t?.(`Stripping demo content for blank template...`),await T(n)),t?.(`Personalizing template...`),await E(n,e.name),e.installDeps&&(t?.(`Installing dependencies...`),g(`pnpm install`,{cwd:n}))}async function C(e){let t=w();if(!n(t))throw Error(`Template tarball missing: ${t}\nRun \`pnpm --filter @murumets-ee/create build:templates\` first.`);await d({file:t,cwd:e})}function w(){let e=c(u(import.meta.url)),t=e;for(let e=0;e<5;e++){let e=l(t,`package.json`);if(n(e))try{if(JSON.parse(r(e,`utf-8`)).name===`@murumets-ee/create`)return l(t,`templates`,`template-demo.tar.gz`)}catch{}let i=c(t);if(i===t)break;t=i}throw Error(`Could not find @murumets-ee/create package root above ${e}. template-demo.tar.gz cannot be located.`)}async function T(e){let t=l(e,`entities`);if(n(t)){let e=await a(t);for(let n of e)n!==`index.ts`&&await o(l(t,n),{force:!0});await s(l(t,`index.ts`),`// Add your entities here and export them.
3
+ // See https://github.com/murumets-ee/lumi-cms for documentation.
4
+
5
+ export const projectEntities = [] as const
6
+ export const projectTaxonomies = [] as const
7
+ `,`utf-8`)}let r=l(e,`seeds`);if(n(r)){let e=await a(r);for(let t of e)t!==`index.ts`&&await o(l(r,t),{force:!0});await s(l(r,`index.ts`),`import type { AdminSeeder } from '@murumets-ee/admin-ui/pages'
8
+
9
+ export const projectSeeders: AdminSeeder[] = []
10
+ `,`utf-8`)}await o(l(e,`app`,`[locale]`,`(shell)`,`demos`),{recursive:!0,force:!0});let c=l(e,`app`,`admin-layout.tsx`);if(n(c)){let e=await i(c,`utf-8`);e=e.replace(/\n\s*\/\/.*demo.*[\s\S]*?const demosGroup[\s\S]*?\}\n/i,`
11
+ `),e=e.replace(`, demosGroup`,``),e=e.replace(/\s*GalleryHorizontalEnd,\n/,`
12
+ `),e=e.replace(/\s*HardDrive,\n/,`
13
+ `),e=e.replace(/\s*Palette,\n/,`
14
+ `),await s(c,e,`utf-8`)}let u=l(e,`toolkit.config.ts`);if(n(u)){let e=await i(u,`utf-8`);e=e.replace(/import \{[^}]+\} from '\.\/entities'/,`import { projectEntities } from './entities'`),e=e.replace(/entities: \[[^\]]+\]/,`entities: [...projectEntities]`),await s(u,e,`utf-8`)}let d=l(e,`lib`,`admin-config.ts`);n(d)&&await s(d,`/**
15
+ * Shared admin configuration — entities + route resource metadata.
16
+ *
17
+ * Add your own entities to the arrays below after defining them in entities/.
308
18
  */
309
19
 
310
- import postgres from 'postgres'
311
-
312
- async function resetDb() {
313
- const DATABASE_URL = process.env.DATABASE_URL
314
-
315
- if (!DATABASE_URL) {
316
- throw new Error('DATABASE_URL not set')
317
- }
318
-
319
- console.warn('WARNING: This will DROP ALL TABLES')
320
- console.log('Database:', DATABASE_URL.split('@')[1])
321
-
322
- const sql = postgres(DATABASE_URL)
323
-
324
- try {
325
- await sql\`
326
- DROP SCHEMA public CASCADE;
327
- CREATE SCHEMA public;
328
- GRANT ALL ON SCHEMA public TO PUBLIC;
329
- \`
330
-
331
- console.log('Database reset complete')
332
- } finally {
333
- await sql.end()
334
- }
335
- }
336
-
337
- resetDb().catch(console.error)
338
- `)}async function k(e,t){let{name:n}=t,r=d(e,`apps/admin`);await a(d(e,`apps`),{recursive:!0}),C(`pnpm create next-app@16 ${r} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await S(r,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`README.md`,`pnpm-workspace.yaml`]),await x(d(r,`package.json`),{dependencies:{[`@${n}/config`]:`workspace:*`,"@murumets-ee/auth-ui":`^${T.myorgAuthUi}`,"better-auth":`^${T.betterAuth}`,"next-intl":`^${T.nextIntl}`,"next-themes":`^${T.nextThemes}`,"lucide-react":`^${T.lucideReact}`,"react-hook-form":`^${T.reactHookForm}`,"@hookform/resolvers":`^${T.hookformResolvers}`,zod:`^${T.zod}`},devDependencies:{"babel-plugin-react-compiler":T.babelReactCompiler}});let{appendToFile:i}=await Promise.resolve().then(()=>y);await i(d(r,`.gitignore`),`
339
- # Environment
340
- .env*
341
- !.env.example
342
- `),await b(d(r,`next.config.ts`),`import type { NextConfig } from 'next'
343
- import createNextIntlPlugin from 'next-intl/plugin'
344
-
345
- const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
346
-
347
- const nextConfig: NextConfig = {
348
- reactCompiler: true,
349
- transpilePackages: [
350
- '@${n}/config',
351
- '@murumets-ee/core',
352
- '@murumets-ee/entity',
353
- '@murumets-ee/db',
354
- '@murumets-ee/logging',
355
- '@murumets-ee/auth-ui',
356
- ],
357
- serverExternalPackages: ['drizzle-orm', 'postgres'],
358
- experimental: {
359
- serverActions: {
360
- bodySizeLimit: '2mb',
361
- },
362
- },
363
- }
364
-
365
- export default withNextIntl(nextConfig)
366
- `),await b(d(r,`proxy.ts`),`import { NextRequest, NextResponse } from 'next/server'
367
- import { getSessionCookie } from 'better-auth/cookies'
368
- import createMiddleware from 'next-intl/middleware'
369
- import { routing } from './i18n/routing'
370
-
371
- const intlMiddleware = createMiddleware(routing)
372
-
373
- const protectedPaths = ['/setup']
374
-
375
- export function proxy(request: NextRequest) {
376
- const { pathname } = request.nextUrl
377
-
378
- const localePattern = new RegExp(\`^/(\${routing.locales.join('|')})\`)
379
- const pathWithoutLocale = pathname.replace(localePattern, '') || '/'
380
-
381
- if (protectedPaths.some((p) => pathWithoutLocale.startsWith(p))) {
382
- const session = getSessionCookie(request)
383
- if (!session) {
384
- const locale = pathname.match(localePattern)?.[1] || routing.defaultLocale
385
- return NextResponse.redirect(new URL(\`/\${locale}/auth/sign-in\`, request.url))
386
- }
387
- }
388
-
389
- return intlMiddleware(request)
390
- }
391
-
392
- export const config = {
393
- matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
394
- }
395
- `),await b(d(r,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
396
-
397
- export const routing = defineRouting({
398
- locales: ['en'],
399
- defaultLocale: 'en',
400
- })
401
- `),await b(d(r,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
402
- import { hasLocale } from 'next-intl'
403
- import { routing } from './routing'
404
- import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
405
-
406
- export default getRequestConfig(async ({ requestLocale }) => {
407
- const requested = await requestLocale
408
- const locale = hasLocale(routing.locales, requested)
409
- ? requested
410
- : routing.defaultLocale
411
-
412
- const authMessages = await getAuthMessages(locale)
413
-
414
- return {
415
- locale,
416
- messages: {
417
- ...authMessages,
418
- },
419
- }
420
- })
421
- `),await b(d(r,`app/globals.css`),`@import "tailwindcss";
422
- @source "../node_modules/@murumets-ee/auth-ui/dist";
423
-
424
- @custom-variant dark (&:where(.dark, .dark *));
425
-
426
- :root {
427
- --background: #ffffff;
428
- --foreground: #171717;
429
- }
430
-
431
- @theme inline {
432
- --color-background: var(--background);
433
- --color-foreground: var(--foreground);
434
- --font-sans: var(--font-geist-sans);
435
- --font-mono: var(--font-geist-mono);
436
- }
437
-
438
- .dark {
439
- --background: #0a0a0a;
440
- --foreground: #ededed;
441
- }
442
-
443
- body {
444
- background: var(--background);
445
- color: var(--foreground);
446
- font-family: Arial, Helvetica, sans-serif;
447
- }
448
- `),await b(d(r,`app/theme-provider.tsx`),`'use client'
449
-
450
- import { ThemeProvider as NextThemesProvider } from 'next-themes'
451
- import type { ReactNode } from 'react'
452
-
453
- export function ThemeProvider({ children }: { children: ReactNode }) {
454
- return (
455
- <NextThemesProvider
456
- attribute="class"
457
- defaultTheme="system"
458
- enableSystem
459
- disableTransitionOnChange
460
- >
461
- {children}
462
- </NextThemesProvider>
463
- )
464
- }
465
- `),await b(d(r,`app/theme-toggle.tsx`),`'use client'
466
-
467
- import { useTheme } from 'next-themes'
468
- import { useState, useEffect } from 'react'
469
- import { Sun, Moon } from 'lucide-react'
470
-
471
- export function ThemeToggle() {
472
- const [mounted, setMounted] = useState(false)
473
- const { resolvedTheme, setTheme } = useTheme()
474
-
475
- useEffect(() => { setMounted(true) }, [])
476
-
477
- if (!mounted) {
478
- return <div className="h-8 w-8" />
479
- }
480
-
481
- return (
482
- <button
483
- type="button"
484
- onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
485
- 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"
486
- aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
487
- >
488
- {resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
489
- </button>
490
- )
491
- }
492
- `),await b(d(r,`app/nav-header.tsx`),`'use client'
493
-
494
- import Link from 'next/link'
495
- import { usePathname } from 'next/navigation'
496
- import { ThemeToggle } from './theme-toggle'
497
-
498
- const links = [
499
- { href: '/', label: 'Home' },
500
- { href: '/auth/sign-in', label: 'Sign In' },
501
- { href: '/setup', label: 'Setup' },
20
+ import { Media } from '@murumets-ee/media'
21
+ import {
22
+ Ticket,
23
+ TicketMessage,
24
+ TicketAttachment,
25
+ Department,
26
+ TicketTag,
27
+ } from '@murumets-ee/ticketing'
28
+
29
+ /** All content entities registered in the admin API. */
30
+ export const allEntities = [
31
+ Media,
32
+ Ticket, TicketMessage, TicketAttachment, Department, TicketTag,
502
33
  ]
503
34
 
504
- export function NavHeader() {
505
- const pathname = usePathname()
506
-
507
- function isActive(href: string) {
508
- return href === '/' ? pathname === '/' : pathname.startsWith(href)
509
- }
510
-
511
- const linkClass = (href: string) =>
512
- \`px-3 py-1.5 rounded-md text-sm transition-colors \${
513
- isActive(href)
514
- ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
515
- : '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'
516
- }\`
517
-
518
- return (
519
- <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">
520
- <nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
521
- <span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
522
- ${n} Admin
523
- </span>
524
- {links.map(({ href, label }) => (
525
- <Link key={href} href={href} className={linkClass(href)}>
526
- {label}
527
- </Link>
528
- ))}
529
- <div className="ml-auto">
530
- <ThemeToggle />
531
- </div>
532
- </nav>
533
- </header>
534
- )
535
- }
536
- `),await b(d(r,`app/layout.tsx`),`import './globals.css'
537
-
538
- export default function RootLayout({ children }: { children: React.ReactNode }) {
539
- return children
540
- }
541
- `),await b(d(r,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
542
- import { Geist, Geist_Mono } from 'next/font/google'
543
- import { NextIntlClientProvider } from 'next-intl'
544
- import { getMessages } from 'next-intl/server'
545
- import { notFound } from 'next/navigation'
546
- import { routing } from '@/i18n/routing'
547
- import { ThemeProvider } from '../theme-provider'
548
- import { NavHeader } from '../nav-header'
549
-
550
- const geistSans = Geist({
551
- variable: '--font-geist-sans',
552
- subsets: ['latin'],
553
- })
554
-
555
- const geistMono = Geist_Mono({
556
- variable: '--font-geist-mono',
557
- subsets: ['latin'],
558
- })
35
+ /** Taxonomy vocabularies. */
36
+ export const taxonomyVocabularies = {}
559
37
 
560
- export const metadata: Metadata = {
561
- title: '${n} Admin',
562
- description: 'Built with Lumi CMS Toolkit',
563
- }
564
-
565
- export default async function LocaleLayout({
566
- children,
567
- params,
568
- }: {
569
- children: React.ReactNode
570
- params: Promise<{ locale: string }>
571
- }) {
572
- const { locale } = await params
573
- if (!routing.locales.includes(locale as any)) notFound()
574
-
575
- const messages = await getMessages()
576
-
577
- return (
578
- <html lang={locale} suppressHydrationWarning>
579
- <body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
580
- <ThemeProvider>
581
- <NextIntlClientProvider locale={locale} messages={messages}>
582
- <NavHeader />
583
- {children}
584
- </NextIntlClientProvider>
585
- </ThemeProvider>
586
- </body>
587
- </html>
588
- )
589
- }
590
- `),await b(d(r,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
591
- import { Article, Category } from '@${n}/config/entities'
592
- import { getToolkitApp } from '@${n}/config/app'
593
-
594
- export default async function HomePage() {
595
- await getToolkitApp()
596
- const articles = createQueryClient(Article)
597
- const categories = createQueryClient(Category)
598
-
599
- const [articleList, categoryList] = await Promise.all([
600
- articles.findMany({ limit: 10 }),
601
- categories.findMany({}),
602
- ])
603
-
604
- return (
605
- <div className="min-h-screen p-8">
606
- <div className="max-w-4xl mx-auto">
607
- <h1 className="text-4xl font-bold mb-8">Welcome to ${n}</h1>
608
-
609
- <section className="mb-12">
610
- <h2 className="text-2xl font-semibold mb-4">
611
- Categories ({categoryList.length})
612
- </h2>
613
- <div className="grid gap-4 md:grid-cols-2">
614
- {categoryList.map((cat) => (
615
- <div
616
- key={cat.id}
617
- className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
618
- >
619
- <h3 className="font-semibold text-lg">{cat.name}</h3>
620
- <p className="text-sm text-zinc-600 dark:text-zinc-400">
621
- {cat.description}
622
- </p>
623
- </div>
624
- ))}
625
- </div>
626
- </section>
627
-
628
- <section>
629
- <h2 className="text-2xl font-semibold mb-4">
630
- Articles ({articleList.length})
631
- </h2>
632
- {articleList.length === 0 ? (
633
- <p className="text-zinc-500">No published articles yet.</p>
634
- ) : (
635
- <div className="space-y-4">
636
- {articleList.map((article) => (
637
- <div
638
- key={article.id}
639
- className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
640
- >
641
- <h3 className="text-xl font-bold">{article.title}</h3>
642
- <p className="text-zinc-600 dark:text-zinc-400 mt-2">
643
- {article.excerpt}
644
- </p>
645
- </div>
646
- ))}
647
- </div>
648
- )}
649
- </section>
650
- </div>
651
- </div>
652
- )
653
- }
654
- `),await b(d(r,`app/api/auth/[...all]/route.ts`),`import { toNextJsHandler } from 'better-auth/next-js'
655
- import { auth } from '@${n}/config/auth'
656
-
657
- const { GET, POST } = toNextJsHandler(auth)
658
- export { GET, POST }
659
- `),await b(d(r,`app/[locale]/auth/layout.tsx`),`import { AuthProviders } from './providers'
660
- import type { ReactNode } from 'react'
661
-
662
- export default function AuthLayout({ children }: { children: ReactNode }) {
663
- return (
664
- <AuthProviders>
665
- <div className="flex min-h-[calc(100vh-3rem)] items-center justify-center px-4">
666
- {children}
667
- </div>
668
- </AuthProviders>
669
- )
670
- }
671
- `),await b(d(r,`app/[locale]/auth/providers.tsx`),`'use client'
672
-
673
- import { AuthUIProvider } from '@murumets-ee/auth-ui'
674
- import { authClient } from '@${n}/config/auth-client'
675
- import Link from 'next/link'
676
- import { useRouter } from 'next/navigation'
677
- import { useLocale } from 'next-intl'
678
- import type { ReactNode } from 'react'
679
-
680
- export function AuthProviders({ children }: { children: ReactNode }) {
681
- const router = useRouter()
682
- const locale = useLocale()
683
-
684
- return (
685
- <AuthUIProvider
686
- authClient={authClient as never}
687
- basePath="/auth"
688
- redirectTo="/"
689
- Link={Link}
690
- navigate={(url) => router.push(url)}
691
- resetPasswordUrl={\`/\${locale}/auth/reset-password\`}
692
- >
693
- {children}
694
- </AuthUIProvider>
695
- )
696
- }
697
- `),await b(d(r,`app/[locale]/auth/sign-in/page.tsx`),`import { SignInForm } from '@murumets-ee/auth-ui'
698
-
699
- export default function SignInPage() {
700
- return <SignInForm />
701
- }
702
- `),await b(d(r,`app/[locale]/auth/sign-up/page.tsx`),`import { SignUpForm } from '@murumets-ee/auth-ui'
703
-
704
- export default function SignUpPage() {
705
- return <SignUpForm />
706
- }
707
- `),await b(d(r,`app/[locale]/auth/forgot-password/page.tsx`),`import { ForgotPasswordForm } from '@murumets-ee/auth-ui'
708
-
709
- export default function ForgotPasswordPage() {
710
- return <ForgotPasswordForm />
711
- }
712
- `),await b(d(r,`app/[locale]/auth/reset-password/page.tsx`),`import { Suspense } from 'react'
713
- import { ResetPasswordContent } from './content'
714
-
715
- export default function ResetPasswordPage() {
716
- return (
717
- <Suspense>
718
- <ResetPasswordContent />
719
- </Suspense>
720
- )
721
- }
722
- `),await b(d(r,`app/[locale]/auth/reset-password/content.tsx`),`'use client'
723
-
724
- import { useSearchParams } from 'next/navigation'
725
- import { ResetPasswordForm } from '@murumets-ee/auth-ui'
726
-
727
- export function ResetPasswordContent() {
728
- const searchParams = useSearchParams()
729
- const token = searchParams.get('token')
730
-
731
- if (!token) {
732
- return (
733
- <div className="text-center">
734
- <h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
735
- Invalid or expired link
736
- </h1>
737
- <p className="mt-2 text-zinc-500">
738
- Please request a new password reset.
739
- </p>
740
- </div>
741
- )
742
- }
743
-
744
- return <ResetPasswordForm token={token} />
745
- }
746
- `),await b(d(r,`app/[locale]/setup/page.tsx`),`import { redirect } from 'next/navigation'
747
- import { sql } from 'drizzle-orm'
748
- import { getToolkitApp } from '@${n}/config/app'
749
- import { SetupForm } from './form'
750
-
751
- export default async function SetupPage() {
752
- const app = await getToolkitApp()
753
- const result = await app.db.readOnly.execute<{ count: string }>(
754
- sql\`SELECT COUNT(*)::text as count FROM "user"\`,
755
- )
756
-
757
- if (Number(result[0]?.count) > 0) {
758
- redirect('/')
759
- }
760
-
761
- return (
762
- <div className="max-w-md mx-auto p-8">
763
- <h1 className="text-2xl font-bold mb-2">Create Admin</h1>
764
- <p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
765
- No users found. Create the first admin account.
766
- </p>
767
- <SetupForm />
768
- </div>
769
- )
770
- }
771
- `),await b(d(r,`app/[locale]/setup/form.tsx`),`'use client'
772
-
773
- import { useState } from 'react'
774
- import { createFirstAdmin } from './actions'
775
-
776
- export function SetupForm() {
777
- const [result, setResult] = useState<{ ok?: boolean; error?: string; email?: string } | null>(null)
778
- const [loading, setLoading] = useState(false)
779
-
780
- async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
781
- e.preventDefault()
782
- setLoading(true)
783
- setResult(null)
784
- const res = await createFirstAdmin(new FormData(e.currentTarget))
785
- setResult(res)
786
- setLoading(false)
787
- }
788
-
789
- if (result?.ok) {
790
- return (
791
- <div className="space-y-2">
792
- <p className="text-green-600 dark:text-green-400">Admin created: {result.email}</p>
793
- <a href="/auth/sign-in" className="text-blue-600 dark:text-blue-400 hover:underline">
794
- Sign in &rarr;
795
- </a>
796
- </div>
797
- )
798
- }
799
-
800
- return (
801
- <form onSubmit={handleSubmit} className="flex flex-col gap-4">
802
- <label className="text-sm font-medium">
803
- Name
804
- <input
805
- name="name"
806
- required
807
- 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"
808
- />
809
- </label>
810
- <label className="text-sm font-medium">
811
- Email
812
- <input
813
- name="email"
814
- type="email"
815
- required
816
- 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"
817
- />
818
- </label>
819
- <label className="text-sm font-medium">
820
- Password (min 8 chars)
821
- <input
822
- name="password"
823
- type="password"
824
- required
825
- minLength={8}
826
- 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"
827
- />
828
- </label>
829
-
830
- {result?.error && <p className="text-red-600 dark:text-red-400 text-sm">{result.error}</p>}
831
-
832
- <button
833
- type="submit"
834
- disabled={loading}
835
- className="px-4 py-2 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 transition-colors"
836
- >
837
- {loading ? 'Creating...' : 'Create Admin'}
838
- </button>
839
- </form>
840
- )
841
- }
842
- `),await b(d(r,`app/[locale]/setup/actions.ts`),`'use server'
843
-
844
- import { sql } from 'drizzle-orm'
845
- import { getToolkitApp } from '@${n}/config/app'
846
- import { getAuth } from '@murumets-ee/auth'
847
-
848
- export async function createFirstAdmin(formData: FormData) {
849
- const email = formData.get('email') as string
850
- const password = formData.get('password') as string
851
- const name = formData.get('name') as string
852
-
853
- if (!email || !password || !name) {
854
- return { error: 'All fields are required' }
855
- }
856
-
857
- if (password.length < 8) {
858
- return { error: 'Password must be at least 8 characters' }
859
- }
860
-
861
- const app = await getToolkitApp()
862
-
863
- const canCreate = await app.db.readWrite.transaction(async (tx) => {
864
- await tx.execute(sql\`SELECT pg_advisory_xact_lock(1)\`)
865
-
866
- const result = await tx.execute<{ count: string }>(
867
- sql\`SELECT COUNT(*)::text as count FROM "user"\`,
868
- )
869
- return Number(result[0]?.count) === 0
870
- })
871
-
872
- if (!canCreate) {
873
- return { error: 'Admin user already exists. Setup is complete.' }
874
- }
875
-
876
- const auth = getAuth()
877
-
878
- const adminUser = await auth.api.signUpEmail({
879
- body: { email, password, name },
880
- })
881
-
882
- await app.db.readWrite.execute(
883
- sql\`UPDATE "user" SET role = 'admin' WHERE id = \${adminUser.user.id}\`,
884
- )
885
-
886
- return { ok: true, email: adminUser.user.email }
887
- }
888
- `)}async function A(e,t){let{name:n}=t,r=d(e,`apps/web`);C(`pnpm create next-app@16 ${r} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await S(r,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`README.md`,`pnpm-workspace.yaml`]),await x(d(r,`package.json`),{dependencies:{[`@${n}/config`]:`workspace:*`,"@murumets-ee/core":`^${T.myorgCore}`,"@murumets-ee/auth-ui":`^${T.myorgAuthUi}`,"next-intl":`^${T.nextIntl}`,"next-themes":`^${T.nextThemes}`,"lucide-react":`^${T.lucideReact}`},devDependencies:{"babel-plugin-react-compiler":T.babelReactCompiler}});let{appendToFile:i}=await Promise.resolve().then(()=>y);await i(d(r,`.gitignore`),`
889
- # Environment
890
- .env*
891
- !.env.example
892
- `),await b(d(r,`next.config.ts`),`import type { NextConfig } from 'next'
893
- import createNextIntlPlugin from 'next-intl/plugin'
894
-
895
- const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
896
-
897
- const nextConfig: NextConfig = {
898
- reactCompiler: true,
899
- transpilePackages: [
900
- '@${n}/config',
901
- '@murumets-ee/core',
902
- '@murumets-ee/entity',
903
- '@murumets-ee/db',
904
- '@murumets-ee/logging',
905
- '@murumets-ee/auth-ui',
906
- ],
907
- serverExternalPackages: ['drizzle-orm', 'postgres'],
908
- }
909
-
910
- export default withNextIntl(nextConfig)
911
- `),await b(d(r,`proxy.ts`),`import createMiddleware from 'next-intl/middleware'
912
- import { routing } from './i18n/routing'
913
-
914
- export default createMiddleware(routing)
915
-
916
- export const config = {
917
- matcher: '/((?!api|_next|_vercel|.*\\\\..*).*)',
918
- }
919
- `),await b(d(r,`i18n/routing.ts`),`import { defineRouting } from 'next-intl/routing'
920
-
921
- export const routing = defineRouting({
922
- locales: ['en'],
923
- defaultLocale: 'en',
924
- })
925
- `),await b(d(r,`i18n/request.ts`),`import { getRequestConfig } from 'next-intl/server'
926
- import { hasLocale } from 'next-intl'
927
- import { routing } from './routing'
928
- import { getAuthMessages } from '@murumets-ee/auth-ui/i18n'
929
-
930
- export default getRequestConfig(async ({ requestLocale }) => {
931
- const requested = await requestLocale
932
- const locale = hasLocale(routing.locales, requested)
933
- ? requested
934
- : routing.defaultLocale
935
-
936
- const authMessages = await getAuthMessages(locale)
937
-
938
- return {
939
- locale,
940
- messages: {
941
- ...authMessages,
942
- },
943
- }
944
- })
945
- `),await b(d(r,`app/globals.css`),`@import "tailwindcss";
946
- @source "../node_modules/@murumets-ee/auth-ui/dist";
947
-
948
- @custom-variant dark (&:where(.dark, .dark *));
949
-
950
- :root {
951
- --background: #ffffff;
952
- --foreground: #171717;
953
- }
954
-
955
- @theme inline {
956
- --color-background: var(--background);
957
- --color-foreground: var(--foreground);
958
- --font-sans: var(--font-geist-sans);
959
- --font-mono: var(--font-geist-mono);
960
- }
961
-
962
- .dark {
963
- --background: #0a0a0a;
964
- --foreground: #ededed;
965
- }
966
-
967
- body {
968
- background: var(--background);
969
- color: var(--foreground);
970
- font-family: Arial, Helvetica, sans-serif;
971
- }
972
- `),await b(d(r,`app/theme-provider.tsx`),`'use client'
973
-
974
- import { ThemeProvider as NextThemesProvider } from 'next-themes'
975
- import type { ReactNode } from 'react'
976
-
977
- export function ThemeProvider({ children }: { children: ReactNode }) {
978
- return (
979
- <NextThemesProvider
980
- attribute="class"
981
- defaultTheme="system"
982
- enableSystem
983
- disableTransitionOnChange
984
- >
985
- {children}
986
- </NextThemesProvider>
987
- )
988
- }
989
- `),await b(d(r,`app/theme-toggle.tsx`),`'use client'
990
-
991
- import { useTheme } from 'next-themes'
992
- import { useState, useEffect } from 'react'
993
- import { Sun, Moon } from 'lucide-react'
994
-
995
- export function ThemeToggle() {
996
- const [mounted, setMounted] = useState(false)
997
- const { resolvedTheme, setTheme } = useTheme()
998
-
999
- useEffect(() => { setMounted(true) }, [])
1000
-
1001
- if (!mounted) {
1002
- return <div className="h-8 w-8" />
1003
- }
1004
-
1005
- return (
1006
- <button
1007
- type="button"
1008
- onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
1009
- 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"
1010
- aria-label={resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
1011
- >
1012
- {resolvedTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
1013
- </button>
1014
- )
1015
- }
1016
- `),await b(d(r,`app/nav-header.tsx`),`'use client'
1017
-
1018
- import Link from 'next/link'
1019
- import { usePathname } from 'next/navigation'
1020
- import { ThemeToggle } from './theme-toggle'
1021
-
1022
- const links = [
1023
- { href: '/', label: 'Home' },
38
+ /** Entities exposed via generic CRUD handler (subset of allEntities). */
39
+ export const crudEntities = [
40
+ Media,
41
+ Ticket, TicketMessage, TicketAttachment,
1024
42
  ]
1025
43
 
1026
- export function NavHeader() {
1027
- const pathname = usePathname()
1028
-
1029
- function isActive(href: string) {
1030
- return href === '/' ? pathname === '/' : pathname.startsWith(href)
1031
- }
1032
-
1033
- const linkClass = (href: string) =>
1034
- \`px-3 py-1.5 rounded-md text-sm transition-colors \${
1035
- isActive(href)
1036
- ? 'bg-zinc-200 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-50'
1037
- : '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'
1038
- }\`
1039
-
1040
- return (
1041
- <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">
1042
- <nav className="max-w-6xl mx-auto flex items-center gap-1 px-4 h-12">
1043
- <span className="font-semibold text-sm text-zinc-700 dark:text-zinc-300 mr-3 select-none">
1044
- ${n}
1045
- </span>
1046
- {links.map(({ href, label }) => (
1047
- <Link key={href} href={href} className={linkClass(href)}>
1048
- {label}
1049
- </Link>
1050
- ))}
1051
- <div className="ml-auto">
1052
- <ThemeToggle />
1053
- </div>
1054
- </nav>
1055
- </header>
1056
- )
1057
- }
1058
- `),await b(d(r,`app/layout.tsx`),`import './globals.css'
1059
-
1060
- export default function RootLayout({ children }: { children: React.ReactNode }) {
1061
- return children
1062
- }
1063
- `),await b(d(r,`app/[locale]/layout.tsx`),`import type { Metadata } from 'next'
1064
- import { Geist, Geist_Mono } from 'next/font/google'
1065
- import { NextIntlClientProvider } from 'next-intl'
1066
- import { getMessages } from 'next-intl/server'
1067
- import { notFound } from 'next/navigation'
1068
- import { routing } from '@/i18n/routing'
1069
- import { ThemeProvider } from '../theme-provider'
1070
- import { NavHeader } from '../nav-header'
1071
-
1072
- const geistSans = Geist({
1073
- variable: '--font-geist-sans',
1074
- subsets: ['latin'],
1075
- })
1076
-
1077
- const geistMono = Geist_Mono({
1078
- variable: '--font-geist-mono',
1079
- subsets: ['latin'],
1080
- })
1081
-
1082
- export const metadata: Metadata = {
1083
- title: '${n}',
1084
- description: 'Built with Lumi CMS Toolkit',
1085
- }
1086
-
1087
- export default async function LocaleLayout({
1088
- children,
1089
- params,
1090
- }: {
1091
- children: React.ReactNode
1092
- params: Promise<{ locale: string }>
1093
- }) {
1094
- const { locale } = await params
1095
- if (!routing.locales.includes(locale as any)) notFound()
1096
-
1097
- const messages = await getMessages()
1098
-
1099
- return (
1100
- <html lang={locale} suppressHydrationWarning>
1101
- <body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`}>
1102
- <ThemeProvider>
1103
- <NextIntlClientProvider locale={locale} messages={messages}>
1104
- <NavHeader />
1105
- {children}
1106
- </NextIntlClientProvider>
1107
- </ThemeProvider>
1108
- </body>
1109
- </html>
1110
- )
1111
- }
1112
- `),await b(d(r,`app/[locale]/page.tsx`),`import { createQueryClient } from '@murumets-ee/core/clients'
1113
- import { Article, Category } from '@${n}/config/entities'
1114
- import { getToolkitApp } from '@${n}/config/app'
1115
-
1116
- export default async function HomePage() {
1117
- await getToolkitApp()
1118
- const articles = createQueryClient(Article)
1119
- const categories = createQueryClient(Category)
1120
-
1121
- const [articleList, categoryList] = await Promise.all([
1122
- articles.findMany({ limit: 10 }),
1123
- categories.findMany({}),
1124
- ])
1125
-
1126
- return (
1127
- <div className="min-h-screen p-8">
1128
- <div className="max-w-4xl mx-auto">
1129
- <h1 className="text-4xl font-bold mb-8">Welcome to ${n}</h1>
1130
-
1131
- <section className="mb-12">
1132
- <h2 className="text-2xl font-semibold mb-4">
1133
- Categories ({categoryList.length})
1134
- </h2>
1135
- <div className="grid gap-4 md:grid-cols-2">
1136
- {categoryList.map((cat) => (
1137
- <div
1138
- key={cat.id}
1139
- className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-4"
1140
- >
1141
- <h3 className="font-semibold text-lg">{cat.name}</h3>
1142
- <p className="text-sm text-zinc-600 dark:text-zinc-400">
1143
- {cat.description}
1144
- </p>
1145
- </div>
1146
- ))}
1147
- </div>
1148
- </section>
44
+ /**
45
+ * Plugin route resource declarations.
46
+ */
47
+ export const pluginResources: { resource: string; actions: readonly string[] }[] = [
48
+ { resource: 'storage', actions: ['view', 'create', 'update', 'delete'] },
49
+ { resource: 'settings', actions: ['view', 'update'] },
50
+ { resource: 'audit-logs', actions: ['view'] },
51
+ { resource: 'permissions', actions: ['view', 'create', 'update', 'delete'] },
52
+ { resource: 'ticketing', actions: ['view', 'create', 'update', 'delete'] },
53
+ ]
54
+ `,`utf-8`);let f=l(e,`app`,`api`,`content`,`[...path]`,`route.ts`);if(n(f)){let e=await i(f,`utf-8`);e=e.replace(/import \{[^}]+\} from '@\/entities'\n/,``),e=e.replace(/entities: \[[^\]]+\]/,`entities: []`),await s(f,e,`utf-8`)}let p=l(e,`app`,`[locale]`,`(shell)`,`admin`,`page.tsx`);n(p)&&await s(p,`import { pages } from '@/lib/admin-pages'
1149
55
 
1150
- <section>
1151
- <h2 className="text-2xl font-semibold mb-4">
1152
- Articles ({articleList.length})
1153
- </h2>
1154
- {articleList.length === 0 ? (
1155
- <p className="text-zinc-500">No published articles yet.</p>
1156
- ) : (
1157
- <div className="space-y-4">
1158
- {articleList.map((article) => (
1159
- <div
1160
- key={article.id}
1161
- className="border border-zinc-200 dark:border-zinc-800 rounded-lg p-6"
1162
- >
1163
- <h3 className="text-xl font-bold">{article.title}</h3>
1164
- <p className="text-zinc-600 dark:text-zinc-400 mt-2">
1165
- {article.excerpt}
1166
- </p>
1167
- </div>
1168
- ))}
1169
- </div>
1170
- )}
1171
- </section>
1172
- </div>
1173
- </div>
1174
- )
1175
- }
1176
- `)}const j=new Set([`.ts`,`.tsx`,`.mts`,`.json`,`.md`,`.yml`,`.yaml`,`.css`,`.html`]),M=new Set([`.env.example`,`Dockerfile`,`.gitignore`]),N=`__PROJECT_NAME__`,P=`__PROJECT_NAME_TITLE__`;async function F(e,t){e.mode===`single`?await I(e,t):await L(e,t)}async function I(e,t){let n=d(process.cwd(),e.name);t?.(`Creating Next.js app...`),C(`pnpm create next-app@16 ${e.name} --typescript --tailwind --no-eslint --app --no-src-dir --turbopack --react-compiler --skip-install --import-alias "@/*"`),await S(n,[`app/page.tsx`,`app/page.module.css`,`app/fonts`,`app/globals.css`,`README.md`]),t?.(`Extracting template...`),await R(e.template,n),t?.(`Personalizing template...`),await B(n,e.name),e.installDeps&&(t?.(`Installing dependencies...`),C(`pnpm install`,{cwd:n}))}async function L(e,t){let n=d(process.cwd(),e.name);t?.(`Creating workspace...`),await E(n,e,t),e.installDeps&&(t?.(`Installing dependencies...`),C(`pnpm install`,{cwd:n}))}async function R(e,t){let r=z(e);if(!n(r))throw Error(`Template tarball missing: ${r}\nRun \`pnpm --filter @murumets-ee/create build:templates\` first.`);await p({file:r,cwd:t})}function z(e){let t=u(f(import.meta.url)),i=t;for(let t=0;t<5;t++){let t=d(i,`package.json`);if(n(t))try{if(JSON.parse(r(t,`utf-8`)).name===`@murumets-ee/create`)return d(i,`templates`,`template-${e}.tar.gz`)}catch{}let a=u(i);if(a===i)break;i=a}throw Error(`Could not find @murumets-ee/create package root above ${t}. template-${e}.tar.gz cannot be located.`)}async function B(e,t){let n=W(t);for await(let r of H(e)){if(!U(r))continue;let e=await o(r,`utf-8`);!e.includes(N)&&!e.includes(P)||await l(r,e.split(P).join(n).split(N).join(t),`utf-8`)}}const V=new Set([`node_modules`,`dist`,`build`,`coverage`,`out`]);async function*H(e){let t=await s(e,{withFileTypes:!0});for(let n of t){let t=d(e,n.name);if(n.isDirectory()){if(V.has(n.name)||n.name.startsWith(`.`))continue;yield*H(t)}else n.isFile()&&(yield t)}}function U(e){let t=e.lastIndexOf(`/`),n=t>=0?e.slice(t+1):e;if(M.has(n))return!0;let r=n.lastIndexOf(`.`);if(r<0)return!1;let i=n.slice(r);return j.has(i)}function W(e){return e.split(/[-_\s]+/).filter(Boolean).map(e=>e[0]?.toUpperCase()+e.slice(1)).join(` `)}async function G(){let n=process.argv[2],r=await v(n);if(!r)return;let i=e.spinner();try{i.start(`Creating Next.js app...`),await F(r,e=>{i.stop(`${t.green(`✓`)} Done`),i.start(e)}),i.stop(`${t.green(`✓`)} Done`);let n=[`cd ${r.name}`,`cp .env.example .env`,`docker compose up -d`,`npx @murumets-ee/cli setup`,`pnpm db:migrate`,`pnpm dev`,`# Visit /setup to create your first admin user`];e.note(n.join(`
1177
- `),`Next steps`),e.outro(`${t.green(`Success!`)} Your project is ready.`)}catch(n){i.stop(t.red(`Failed.`)),e.log.error(n instanceof Error?n.message:String(n)),process.exit(1)}}G();export{};
56
+ export default pages.DashboardPage
57
+ `,`utf-8`)}async function E(e,t){let n=A(t);for await(let r of O(e)){if(!k(r))continue;let e=await i(r,`utf-8`);!e.includes(y)&&!e.includes(b)||await s(r,e.split(b).join(n).split(y).join(t),`utf-8`)}}const D=new Set([`node_modules`,`dist`,`build`,`coverage`,`out`]);async function*O(e){let t=await a(e,{withFileTypes:!0});for(let n of t){let t=l(e,n.name);if(n.isDirectory()){if(D.has(n.name)||n.name.startsWith(`.`))continue;yield*O(t)}else n.isFile()&&(yield t)}}function k(e){let t=e.lastIndexOf(`/`),n=t>=0?e.slice(t+1):e;if(v.has(n))return!0;let r=n.lastIndexOf(`.`);if(r<0)return!1;let i=n.slice(r);return _.has(i)}function A(e){return e.split(/[-_\s]+/).filter(Boolean).map(e=>e[0]?.toUpperCase()+e.slice(1)).join(` `)}async function j(){let n=process.argv[2],r=await m(n);if(!r)return;let i=e.spinner();try{i.start(`Creating Next.js app...`),await x(r,e=>{i.stop(`${t.green(`✓`)} Done`),i.start(e)}),i.stop(`${t.green(`✓`)} Done`);let n=[`cd ${r.name}`,`cp .env.example .env`,`docker compose up -d`,`npx @murumets-ee/cli setup`,`pnpm db:migrate`,`pnpm dev`,`# Visit /setup to create your first admin user`];e.note(n.join(`
58
+ `),`Next steps`),e.outro(`${t.green(`Success!`)} Your project is ready.`)}catch(n){i.stop(t.red(`Failed.`)),e.log.error(n instanceof Error?n.message:String(n)),process.exit(1)}}j();export{};