@infuro/cms-core 1.0.6 → 1.0.7

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.cjs ADDED
@@ -0,0 +1,859 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_fs = __toESM(require("fs"), 1);
28
+ var import_path = __toESM(require("path"), 1);
29
+ var TEMPLATES = {
30
+ "src/lib/data-source.ts": `import 'reflect-metadata';
31
+ import path from 'path';
32
+ import { createRequire } from 'module';
33
+ import { DataSource } from 'typeorm';
34
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const coreDir = path.dirname(require.resolve('@infuro/cms-core'));
38
+
39
+ let dataSource: DataSource | null = null;
40
+
41
+ export function getDataSource(): DataSource {
42
+ if (!dataSource) {
43
+ dataSource = new DataSource({
44
+ type: 'postgres',
45
+ url: process.env.DATABASE_URL,
46
+ entities: Object.values(CMS_ENTITY_MAP),
47
+ synchronize: false,
48
+ ...(process.env.TYPEORM_CLI && {
49
+ migrations: [
50
+ path.join(coreDir, 'migrations', '*.ts'),
51
+ path.join(process.cwd(), 'src', 'migrations', '*.ts'),
52
+ ],
53
+ }),
54
+ });
55
+ }
56
+ return dataSource;
57
+ }
58
+
59
+ export async function getDataSourceInitialized(): Promise<DataSource> {
60
+ const ds = getDataSource();
61
+ if (!ds.isInitialized) await ds.initialize();
62
+ return ds;
63
+ }
64
+
65
+ export default getDataSource;
66
+ `,
67
+ "src/lib/auth-helpers.ts": `import { getServerSession } from 'next-auth';
68
+ import { NextResponse } from 'next/server';
69
+ import { createAuthHelpers } from '@infuro/cms-core/auth';
70
+
71
+ const helpers = createAuthHelpers(
72
+ async () => {
73
+ const s = await getServerSession();
74
+ return s ? { user: s.user } : null;
75
+ },
76
+ NextResponse
77
+ );
78
+
79
+ export const requireAuth = helpers.requireAuth;
80
+ export const requirePermission = helpers.requirePermission;
81
+ export const getAuthenticatedUser = helpers.getAuthenticatedUser;
82
+ `,
83
+ "src/lib/cms.ts": `import {
84
+ createCmsApp,
85
+ localStoragePlugin,
86
+ type CmsApp,
87
+ } from '@infuro/cms-core';
88
+ import { getDataSourceInitialized } from './data-source';
89
+
90
+ let cmsPromise: Promise<CmsApp> | null = null;
91
+
92
+ export async function getCms(): Promise<CmsApp> {
93
+ if (cmsPromise) return cmsPromise;
94
+ const dataSource = await getDataSourceInitialized();
95
+ cmsPromise = createCmsApp({
96
+ dataSource,
97
+ config: process.env as unknown as Record<string, string>,
98
+ plugins: [
99
+ localStoragePlugin({ dir: 'public/uploads' }),
100
+ ],
101
+ });
102
+ return cmsPromise;
103
+ }
104
+ `,
105
+ "src/app/api/[[...path]]/route.ts": `import { NextResponse } from 'next/server';
106
+ import { getServerSession } from 'next-auth';
107
+ import { createCmsApiHandler } from '@infuro/cms-core/api';
108
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
109
+ import { getDataSourceInitialized } from '@/lib/data-source';
110
+ import { requireAuth } from '@/lib/auth-helpers';
111
+ import { getCms } from '@/lib/cms';
112
+ import bcrypt from 'bcryptjs';
113
+
114
+ const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000';
115
+
116
+ let handlerPromise: Promise<ReturnType<typeof createCmsApiHandler>> | null = null;
117
+
118
+ async function getHandler() {
119
+ if (!handlerPromise) {
120
+ const dataSource = await getDataSourceInitialized();
121
+ handlerPromise = Promise.resolve(
122
+ createCmsApiHandler({
123
+ dataSource,
124
+ entityMap: CMS_ENTITY_MAP,
125
+ requireAuth,
126
+ json: NextResponse.json.bind(NextResponse),
127
+ getCms,
128
+ userAuth: {
129
+ dataSource,
130
+ entityMap: CMS_ENTITY_MAP,
131
+ json: NextResponse.json.bind(NextResponse),
132
+ baseUrl,
133
+ hashPassword: (p) => Promise.resolve(bcrypt.hashSync(p, 12)),
134
+ comparePassword: (p, h) => Promise.resolve(bcrypt.compareSync(p, h)),
135
+ resetExpiryHours: 1,
136
+ getSession: () =>
137
+ getServerSession().then((s) => (s ? { user: s.user } : null)),
138
+ },
139
+ dashboard: {
140
+ dataSource,
141
+ entityMap: CMS_ENTITY_MAP,
142
+ json: NextResponse.json.bind(NextResponse),
143
+ requireAuth,
144
+ requirePermission: requireAuth,
145
+ },
146
+ upload: {
147
+ json: NextResponse.json.bind(NextResponse),
148
+ requireAuth,
149
+ storage: () => getCms().then((cms) => cms.getPlugin('storage')),
150
+ localUploadDir: 'public/uploads',
151
+ },
152
+ blogBySlug: {
153
+ dataSource,
154
+ entityMap: CMS_ENTITY_MAP,
155
+ json: NextResponse.json.bind(NextResponse),
156
+ requireAuth: async () => null,
157
+ },
158
+ formBySlug: {
159
+ dataSource,
160
+ entityMap: CMS_ENTITY_MAP,
161
+ json: NextResponse.json.bind(NextResponse),
162
+ requireAuth: async () => null,
163
+ },
164
+ usersApi: {
165
+ dataSource,
166
+ entityMap: CMS_ENTITY_MAP,
167
+ json: NextResponse.json.bind(NextResponse),
168
+ requireAuth,
169
+ baseUrl,
170
+ },
171
+ })
172
+ );
173
+ }
174
+ return handlerPromise;
175
+ }
176
+
177
+ async function handle(method: string, req: Request, context: { params: Promise<{ path?: string[] }> }) {
178
+ try {
179
+ const handler = await getHandler();
180
+ const { path: pathSegments = [] } = await context.params;
181
+ return handler.handle(method, pathSegments, req);
182
+ } catch {
183
+ return NextResponse.json({ error: 'Server Error' }, { status: 500 });
184
+ }
185
+ }
186
+
187
+ export async function GET(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('GET', req, ctx); }
188
+ export async function POST(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('POST', req, ctx); }
189
+ export async function PUT(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PUT', req, ctx); }
190
+ export async function PATCH(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('PATCH', req, ctx); }
191
+ export async function DELETE(req: Request, ctx: { params: Promise<{ path?: string[] }> }) { return handle('DELETE', req, ctx); }
192
+ `,
193
+ "src/app/api/auth/[...nextauth]/route.ts": `import NextAuth from 'next-auth';
194
+ import { getNextAuthOptions } from '@infuro/cms-core/auth';
195
+ import { getDataSourceInitialized } from '@/lib/data-source';
196
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
197
+ import bcrypt from 'bcryptjs';
198
+
199
+ async function getOptions() {
200
+ const dataSource = await getDataSourceInitialized();
201
+ const userRepo = dataSource.getRepository(CMS_ENTITY_MAP.users);
202
+ return getNextAuthOptions({
203
+ getUserByEmail: async (email: string) => {
204
+ return userRepo.findOne({
205
+ where: { email },
206
+ relations: ['group', 'group.permissions'],
207
+ select: ['id', 'email', 'name', 'password', 'blocked', 'deleted', 'groupId'],
208
+ }) as any;
209
+ },
210
+ comparePassword: (plain, hash) => Promise.resolve(bcrypt.compareSync(plain, hash)),
211
+ signInPage: '/admin/signin',
212
+ });
213
+ }
214
+
215
+ let handler: ReturnType<typeof NextAuth> | null = null;
216
+
217
+ async function getHandler() {
218
+ if (!handler) handler = NextAuth(await getOptions());
219
+ return handler;
220
+ }
221
+
222
+ type NextAuthContext = { params: Promise<{ nextauth?: string[] }> };
223
+
224
+ export async function GET(req: Request, context: NextAuthContext) {
225
+ return (await getHandler())(req, context);
226
+ }
227
+ export async function POST(req: Request, context: NextAuthContext) {
228
+ return (await getHandler())(req, context);
229
+ }
230
+ `,
231
+ "src/app/admin/layout.tsx": `'use client';
232
+
233
+ import '@infuro/cms-core/admin.css';
234
+ import AdminLayout from '@infuro/cms-core/admin';
235
+
236
+ export default function AdminLayoutWrapper({ children }: { children: React.ReactNode }) {
237
+ return (
238
+ <AdminLayout
239
+ customNavItems={[]}
240
+ customNavSections={[]}
241
+ customCrudConfigs={{}}
242
+ >
243
+ {children}
244
+ </AdminLayout>
245
+ );
246
+ }
247
+ `,
248
+ "src/app/admin/[[...slug]]/page.tsx": `import { AdminPageResolver } from '@infuro/cms-core/admin';
249
+
250
+ export default async function AdminPage({ params }: { params: Promise<{ slug?: string[] }> }) {
251
+ const { slug } = await params;
252
+ return <AdminPageResolver slug={slug} />;
253
+ }
254
+ `,
255
+ "src/middleware.ts": `import { NextResponse } from 'next/server';
256
+ import type { NextRequest } from 'next/server';
257
+ import { createCmsMiddleware } from '@infuro/cms-core/auth';
258
+
259
+ const cmsMiddleware = createCmsMiddleware({
260
+ publicApiMethods: {
261
+ '/api/contacts': ['POST'],
262
+ '/api/form-submissions': ['POST'],
263
+ '/api/blogs': ['GET'],
264
+ '/api/forms': ['GET'],
265
+ '/api/auth': ['GET', 'POST'],
266
+ '/api/users/forgot-password': ['POST'],
267
+ '/api/users/set-password': ['POST'],
268
+ '/api/users/invite': ['POST'],
269
+ },
270
+ });
271
+
272
+ export function middleware(request: NextRequest) {
273
+ const result = cmsMiddleware({
274
+ nextUrl: request.nextUrl,
275
+ url: request.url,
276
+ method: request.method,
277
+ cookies: request.cookies,
278
+ });
279
+
280
+ if (result.type === 'next') return NextResponse.next();
281
+ if (result.type === 'redirect') return NextResponse.redirect(result.url);
282
+ if (result.type === 'json') return NextResponse.json(result.body, { status: result.status });
283
+ return NextResponse.next();
284
+ }
285
+
286
+ export const config = {
287
+ matcher: ['/admin/:path*', '/api/:path*'],
288
+ };
289
+ `,
290
+ "src/app/providers.tsx": `"use client";
291
+
292
+ import { ThemeProvider } from "next-themes";
293
+ import { SessionProvider } from "next-auth/react";
294
+ import { Toaster } from "sonner";
295
+
296
+ export function Providers({ children }: { children: React.ReactNode }) {
297
+ return (
298
+ <SessionProvider>
299
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
300
+ {children}
301
+ <Toaster position="top-right" />
302
+ </ThemeProvider>
303
+ </SessionProvider>
304
+ );
305
+ }
306
+ `,
307
+ ".env.example": `DATABASE_URL=postgres://user:password@localhost:5432/mydb
308
+ NEXTAUTH_SECRET=your-random-secret
309
+ NEXTAUTH_URL=http://localhost:3000
310
+
311
+ # Admin user (for npm run seed)
312
+ ADMIN_EMAIL=admin@example.com
313
+ ADMIN_PASSWORD=changeme
314
+ `,
315
+ "src/lib/seed.ts": `try { require('dotenv/config'); } catch {}
316
+ import 'reflect-metadata';
317
+ import { getDataSourceInitialized } from './data-source';
318
+ import { CMS_ENTITY_MAP } from '@infuro/cms-core';
319
+ import bcrypt from 'bcryptjs';
320
+
321
+ async function main() {
322
+ const ds = await getDataSourceInitialized();
323
+ const userRepo = ds.getRepository(CMS_ENTITY_MAP.users);
324
+
325
+ const email = process.env.ADMIN_EMAIL || 'admin@example.com';
326
+ const password = process.env.ADMIN_PASSWORD || 'changeme';
327
+
328
+ const existing = await userRepo.findOne({ where: { email } });
329
+ if (!existing) {
330
+ const hashedPassword = await bcrypt.hash(password, 10);
331
+ await userRepo.save(userRepo.create({ name: 'Admin', email, password: hashedPassword }));
332
+ console.log('Default admin user created');
333
+ } else {
334
+ console.log('Default admin user already exists');
335
+ }
336
+
337
+ await ds.destroy();
338
+ }
339
+
340
+ main().catch((e) => {
341
+ console.error(e);
342
+ process.exit(1);
343
+ });
344
+ `,
345
+ "scripts/migration-datasource.cjs": `/**
346
+ * Data source for TypeORM CLI (migration:generate). Resolves @infuro/cms-core from project root (works with npm link).
347
+ */
348
+ require('reflect-metadata');
349
+ require('dotenv/config');
350
+ const path = require('path');
351
+ const { DataSource } = require('typeorm');
352
+ const coreEntry = path.resolve(__dirname, '..', 'node_modules', '@infuro', 'cms-core', 'dist', 'index.cjs');
353
+ const { CMS_ENTITY_MAP } = require(coreEntry);
354
+
355
+ module.exports = new DataSource({
356
+ type: 'postgres',
357
+ url: process.env.DATABASE_URL,
358
+ entities: Object.values(CMS_ENTITY_MAP),
359
+ synchronize: false,
360
+ migrations: ['src/migrations/*.ts'],
361
+ });
362
+ `,
363
+ "scripts/run-migrations.ts": `/**
364
+ * Run TypeORM migrations. Loads .env so DATABASE_URL is set.
365
+ * Usage: npm run migration:run
366
+ */
367
+ try { require('dotenv/config'); } catch {}
368
+ import 'reflect-metadata';
369
+
370
+ async function main() {
371
+ process.env.TYPEORM_CLI = '1';
372
+ const { getDataSourceInitialized } = await import('../src/lib/data-source');
373
+ const ds = await getDataSourceInitialized();
374
+ const run = await ds.runMigrations();
375
+ console.log(run.length ? \`Ran \${run.length} migration(s).\` : 'No pending migrations.');
376
+ await ds.destroy();
377
+ }
378
+
379
+ main().catch((e) => {
380
+ console.error(e);
381
+ process.exit(1);
382
+ });
383
+ `,
384
+ "src/migrations/README.md": `# TypeORM migrations
385
+
386
+ Generate a new migration (after changing entities):
387
+
388
+ \`\`\`bash
389
+ npm run migration:generate -- MyMigrationName
390
+ \`\`\`
391
+
392
+ Run pending migrations:
393
+
394
+ \`\`\`bash
395
+ npm run migration:run
396
+ \`\`\`
397
+ `,
398
+ "src/themes/default/index.ts": `import { createTheme } from '@infuro/cms-core/theme';
399
+
400
+ import { Container, meta as containerMeta } from './components/Container';
401
+ import { TextBlock, meta as textBlockMeta } from './components/TextBlock';
402
+
403
+ import { Navbar } from './layout/Navbar';
404
+ import { Footer, footerFields, footerDefaults } from './layout/Footer';
405
+
406
+ export default createTheme({
407
+ name: 'default',
408
+ label: 'Default Theme',
409
+ components: [
410
+ { component: Container, meta: containerMeta },
411
+ { component: TextBlock, meta: textBlockMeta },
412
+ ],
413
+ layout: {
414
+ navbar: { component: Navbar },
415
+ footer: { component: Footer, fields: footerFields, defaults: footerDefaults },
416
+ },
417
+ });
418
+ `,
419
+ "src/themes/default/layout/Navbar.tsx": `import type { NavbarConfig, NavItem } from '@infuro/cms-core/theme';
420
+
421
+ function NavLink({ item }: { item: NavItem }) {
422
+ return (
423
+ <li className="list-none">
424
+ <a
425
+ href={item.url}
426
+ target={item.openInNewTab ? '_blank' : undefined}
427
+ rel={item.openInNewTab ? 'noopener noreferrer' : undefined}
428
+ className="text-sm font-medium text-gray-700 hover:text-gray-900 px-3 py-2 inline-block"
429
+ >
430
+ {item.label}
431
+ </a>
432
+ </li>
433
+ );
434
+ }
435
+
436
+ export function Navbar({ logo, items, ctaLabel, ctaUrl }: NavbarConfig) {
437
+ return (
438
+ <nav className="bg-white sticky top-0 z-40 border-b">
439
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
440
+ <div className="flex items-center justify-between h-16">
441
+ <a href="/" className="flex-shrink-0">
442
+ {logo ? (
443
+ <img src={logo} alt="Logo" className="h-8" />
444
+ ) : (
445
+ <span className="text-xl font-bold text-gray-900">Logo</span>
446
+ )}
447
+ </a>
448
+ <ul className="flex items-center gap-1 list-none m-0 p-0">
449
+ {items.map((item) => (
450
+ <NavLink key={item.id} item={item} />
451
+ ))}
452
+ </ul>
453
+ {ctaLabel && (
454
+ <a href={ctaUrl || '#'} className="text-sm font-medium text-gray-900 hover:underline">
455
+ {ctaLabel}
456
+ </a>
457
+ )}
458
+ </div>
459
+ </div>
460
+ </nav>
461
+ );
462
+ }
463
+ `,
464
+ "src/themes/default/layout/Footer.tsx": `import type { FooterConfig, PropDefinition } from '@infuro/cms-core/theme';
465
+
466
+ export const footerFields: PropDefinition[] = [
467
+ { name: 'copyright', label: 'Copyright Text', type: 'text' },
468
+ ];
469
+
470
+ export const footerDefaults: Record<string, any> = {
471
+ copyright: '\xA9 2025 Your Site. All rights reserved.',
472
+ columns: [],
473
+ socialLinks: [],
474
+ };
475
+
476
+ export function Footer({ copyright, columns = [], socialLinks = [] }: FooterConfig) {
477
+ return (
478
+ <footer className="bg-gray-900 text-gray-400 py-8 px-8">
479
+ <div className="max-w-7xl mx-auto">
480
+ {columns.length > 0 && (
481
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-6">
482
+ {columns.map((col, i) => (
483
+ <div key={i}>
484
+ <h4 className="text-white font-semibold mb-3 text-sm">{col.title}</h4>
485
+ <ul className="space-y-2">
486
+ {col.links.map((link, j) => (
487
+ <li key={j}>
488
+ <a href={link.url} className="text-sm hover:text-white transition-colors">
489
+ {link.label}
490
+ </a>
491
+ </li>
492
+ ))}
493
+ </ul>
494
+ </div>
495
+ ))}
496
+ </div>
497
+ )}
498
+ <div className="border-t border-gray-800 pt-6">
499
+ <p className="text-sm">{copyright}</p>
500
+ </div>
501
+ </div>
502
+ </footer>
503
+ );
504
+ }
505
+ `,
506
+ "src/themes/default/components/Container.tsx": `import type { ComponentMeta } from '@infuro/cms-core/theme';
507
+
508
+ export const meta: ComponentMeta = {
509
+ name: 'Container',
510
+ label: 'Container',
511
+ category: 'layout',
512
+ icon: 'LayoutDashboard',
513
+ description: 'A layout container that holds other components',
514
+ defaultProps: { background: '#ffffff' },
515
+ props: [{ name: 'background', label: 'Background', type: 'color' }],
516
+ canContainChildren: true,
517
+ };
518
+
519
+ export function Container({
520
+ background = '#ffffff',
521
+ children,
522
+ }: {
523
+ background?: string;
524
+ children?: React.ReactNode;
525
+ }) {
526
+ return (
527
+ <div style={{ backgroundColor: background, minHeight: '48px', padding: '1rem' }}>
528
+ {children}
529
+ </div>
530
+ );
531
+ }
532
+ `,
533
+ "src/themes/default/components/TextBlock.tsx": `import type { ComponentMeta } from '@infuro/cms-core/theme';
534
+
535
+ export const meta: ComponentMeta = {
536
+ name: 'TextBlock',
537
+ label: 'Text Block',
538
+ category: 'content',
539
+ icon: 'Type',
540
+ description: 'Rich text content block',
541
+ defaultProps: { content: '<p>Enter your text here...</p>' },
542
+ props: [{ name: 'content', label: 'Content', type: 'richtext' }],
543
+ };
544
+
545
+ export function TextBlock({ content }: { content?: string }) {
546
+ return (
547
+ <div
548
+ className="prose prose-gray max-w-none py-4 px-2"
549
+ dangerouslySetInnerHTML={{ __html: content || '' }}
550
+ />
551
+ );
552
+ }
553
+ `,
554
+ "src/lib/theme-registry.ts": `import defaultTheme from '@/themes/default';
555
+ import type { ThemeConfig } from '@infuro/cms-core/theme';
556
+
557
+ export const defaultThemeConfig = defaultTheme;
558
+
559
+ export interface ThemeRegistryItem {
560
+ id: string;
561
+ label: string;
562
+ config: ThemeConfig;
563
+ description?: string;
564
+ }
565
+
566
+ export const THEME_REGISTRY: ThemeRegistryItem[] = [
567
+ {
568
+ id: 'default',
569
+ label: 'Default',
570
+ config: defaultTheme,
571
+ description: 'Default theme with standard layout and components.',
572
+ },
573
+ ];
574
+
575
+ export function getThemeById(id: string | undefined): ThemeConfig {
576
+ const theme = THEME_REGISTRY.find((t) => t.id === (id || '')) ?? THEME_REGISTRY[0];
577
+ return theme?.config ?? defaultTheme;
578
+ }
579
+ `,
580
+ "src/app/page.tsx": `export default function HomePage() {
581
+ return (
582
+ <main className="min-h-screen flex flex-col items-center justify-center p-8">
583
+ <h1 className="text-3xl font-bold text-gray-900 mb-4">Welcome</h1>
584
+ <p className="text-gray-600 mb-6">Your CMS is set up. Manage content at the admin panel.</p>
585
+ <a
586
+ href="/admin"
587
+ className="text-white bg-gray-900 hover:bg-gray-800 px-4 py-2 rounded-lg font-medium transition-colors"
588
+ >
589
+ Open Admin
590
+ </a>
591
+ </main>
592
+ );
593
+ }
594
+ `,
595
+ "src/app/contact/page.tsx": `export default function ContactPage() {
596
+ return (
597
+ <main className="min-h-screen p-8 max-w-2xl mx-auto">
598
+ <h1 className="text-3xl font-bold text-gray-900 mb-4">Contact</h1>
599
+ <p className="text-gray-600">
600
+ Add a contact form or wire this page to your CMS form. Use the admin panel to manage forms and submissions.
601
+ </p>
602
+ </main>
603
+ );
604
+ }
605
+ `
606
+ };
607
+ function findRoot(cwd) {
608
+ let dir = import_path.default.resolve(cwd);
609
+ for (let i = 0; i < 20; i++) {
610
+ const pkgPath = import_path.default.join(dir, "package.json");
611
+ if (import_fs.default.existsSync(pkgPath)) {
612
+ try {
613
+ const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf8"));
614
+ if (pkg.dependencies?.next || pkg.devDependencies?.next) return dir;
615
+ } catch {
616
+ }
617
+ }
618
+ const parent = import_path.default.dirname(dir);
619
+ if (parent === dir) break;
620
+ dir = parent;
621
+ }
622
+ return null;
623
+ }
624
+ function writeFile(root, filePath, content, force2, dryRun2, log) {
625
+ const full = import_path.default.join(root, filePath);
626
+ if (import_fs.default.existsSync(full) && !force2) {
627
+ log(` skip (exists): ${filePath}`);
628
+ return false;
629
+ }
630
+ if (dryRun2) {
631
+ log(` would create: ${filePath}`);
632
+ return true;
633
+ }
634
+ const dir = import_path.default.dirname(full);
635
+ import_fs.default.mkdirSync(dir, { recursive: true });
636
+ import_fs.default.writeFileSync(full, content, "utf8");
637
+ log(` created: ${filePath}`);
638
+ return true;
639
+ }
640
+ function patchNextConfig(root, dryRun2, log) {
641
+ const candidates = ["next.config.js", "next.config.mjs", "next.config.cjs"];
642
+ let configPath = null;
643
+ for (const name of candidates) {
644
+ const p = import_path.default.join(root, name);
645
+ if (import_fs.default.existsSync(p)) {
646
+ configPath = p;
647
+ break;
648
+ }
649
+ }
650
+ if (!configPath) {
651
+ log(" skip next.config: not found");
652
+ return false;
653
+ }
654
+ let content = import_fs.default.readFileSync(configPath, "utf8");
655
+ if (content.includes("'@infuro/cms-core'") || content.includes('"@infuro/cms-core"')) {
656
+ log(` skip (already has core): ${import_path.default.basename(configPath)}`);
657
+ return false;
658
+ }
659
+ if (content.includes("serverExternalPackages")) {
660
+ content = content.replace(
661
+ /(serverExternalPackages:\s*\[)/,
662
+ "$1'@infuro/cms-core', 'typeorm', "
663
+ );
664
+ } else {
665
+ content = content.replace(
666
+ /(const nextConfig\s*=\s*\{|module\.exports\s*=\s*\{)/,
667
+ "$1\n serverExternalPackages: ['@infuro/cms-core', 'typeorm'],"
668
+ );
669
+ }
670
+ if (dryRun2) {
671
+ log(` would patch: ${import_path.default.basename(configPath)}`);
672
+ return true;
673
+ }
674
+ import_fs.default.writeFileSync(configPath, content, "utf8");
675
+ log(` patched: ${import_path.default.basename(configPath)}`);
676
+ return true;
677
+ }
678
+ function patchTailwind(root, dryRun2, log) {
679
+ const candidates = ["tailwind.config.js", "tailwind.config.mjs", "tailwind.config.ts"];
680
+ let configPath = null;
681
+ for (const name of candidates) {
682
+ const p = import_path.default.join(root, name);
683
+ if (import_fs.default.existsSync(p)) {
684
+ configPath = p;
685
+ break;
686
+ }
687
+ }
688
+ if (!configPath) {
689
+ log(" skip tailwind: config not found");
690
+ return false;
691
+ }
692
+ let content = import_fs.default.readFileSync(configPath, "utf8");
693
+ const coreContent = "./node_modules/@infuro/cms-core/dist/**/*.{js,cjs}";
694
+ if (content.includes("@infuro/cms-core")) {
695
+ log(` skip (already has core): ${import_path.default.basename(configPath)}`);
696
+ return false;
697
+ }
698
+ if (content.includes("content:")) {
699
+ content = content.replace(
700
+ /(content:\s*\[)/,
701
+ `$1
702
+ "${coreContent}",`
703
+ );
704
+ }
705
+ if (dryRun2) {
706
+ log(` would patch: ${import_path.default.basename(configPath)}`);
707
+ return true;
708
+ }
709
+ import_fs.default.writeFileSync(configPath, content, "utf8");
710
+ log(` patched: ${import_path.default.basename(configPath)}`);
711
+ return true;
712
+ }
713
+ function patchLayout(root, dryRun2, log) {
714
+ const layoutPath = import_path.default.join(root, "src/app/layout.tsx");
715
+ if (!import_fs.default.existsSync(layoutPath)) {
716
+ log(" skip layout: src/app/layout.tsx not found");
717
+ return false;
718
+ }
719
+ let content = import_fs.default.readFileSync(layoutPath, "utf8");
720
+ if (content.includes("<Providers>")) {
721
+ log(" skip layout: already uses Providers");
722
+ return false;
723
+ }
724
+ const bodyMatch = content.match(/<body([^>]*)>\s*(\{children\})\s*<\/body>/s);
725
+ if (!bodyMatch) {
726
+ log(" skip layout: unexpected structure (add <Providers> manually)");
727
+ return false;
728
+ }
729
+ if (dryRun2) {
730
+ log(" would patch: src/app/layout.tsx");
731
+ return true;
732
+ }
733
+ const [, bodyAttrs, children] = bodyMatch;
734
+ const newBody = `<body${bodyAttrs}>
735
+ <Providers>${children}</Providers>
736
+ </body>`;
737
+ content = content.replace(/<body[^>]*>\s*\{children\}\s*<\/body>/s, newBody);
738
+ if (!content.includes("from './providers'") && !content.includes('from "./providers"')) {
739
+ const firstImport = content.match(/^import .+ from .+;\n/m);
740
+ content = firstImport ? content.replace(firstImport[0], firstImport[0] + "import { Providers } from './providers';\n") : "import { Providers } from './providers';\n" + content;
741
+ }
742
+ if (content.includes("<html") && !content.includes("suppressHydrationWarning")) {
743
+ content = content.replace(/<html(\s)/, "<html suppressHydrationWarning$1");
744
+ }
745
+ import_fs.default.writeFileSync(layoutPath, content, "utf8");
746
+ log(" patched: src/app/layout.tsx");
747
+ return true;
748
+ }
749
+ function patchPackageJson(root, dryRun2, log) {
750
+ const pkgPath = import_path.default.join(root, "package.json");
751
+ if (!import_fs.default.existsSync(pkgPath)) return false;
752
+ const pkg = JSON.parse(import_fs.default.readFileSync(pkgPath, "utf8"));
753
+ const scripts = pkg.scripts || {};
754
+ let changed = false;
755
+ if (!scripts.seed) {
756
+ scripts.seed = "tsx src/lib/seed.ts";
757
+ changed = true;
758
+ }
759
+ if (!scripts["migration:run"]) {
760
+ scripts["migration:run"] = "tsx scripts/run-migrations.ts";
761
+ changed = true;
762
+ }
763
+ const dev = pkg.devDependencies || {};
764
+ const deps = pkg.dependencies || {};
765
+ if (!deps["@infuro/cms-core"]) {
766
+ deps["@infuro/cms-core"] = "^1.0.6";
767
+ changed = true;
768
+ }
769
+ if (!dev.tsx) {
770
+ dev.tsx = "^4.0.0";
771
+ changed = true;
772
+ }
773
+ if (!dev.dotenv) {
774
+ dev.dotenv = "^16.0.0";
775
+ changed = true;
776
+ }
777
+ if (!changed) {
778
+ log(" skip package.json: scripts/devDeps already present");
779
+ return false;
780
+ }
781
+ pkg.scripts = scripts;
782
+ pkg.dependencies = { ...pkg.dependencies, ...deps };
783
+ pkg.devDependencies = { ...pkg.devDependencies, ...dev };
784
+ if (dryRun2) {
785
+ log(" would patch: package.json (scripts + devDependencies)");
786
+ return true;
787
+ }
788
+ import_fs.default.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2), "utf8");
789
+ log(" patched: package.json");
790
+ return true;
791
+ }
792
+ async function runNpmInstall(root, log) {
793
+ const { spawnSync } = await import("child_process");
794
+ log(" running: npm install (deps)...");
795
+ spawnSync("npm", ["install", "@infuro/cms-core", "typeorm", "reflect-metadata", "bcryptjs", "next-auth", "next-themes", "sonner"], {
796
+ cwd: root,
797
+ stdio: "inherit",
798
+ shell: true
799
+ });
800
+ log(" running: npm install -D tsx dotenv @types/node...");
801
+ spawnSync("npm", ["install", "-D", "tsx", "dotenv", "@types/node"], {
802
+ cwd: root,
803
+ stdio: "inherit",
804
+ shell: true
805
+ });
806
+ }
807
+ async function runInit(opts) {
808
+ const log = (msg) => console.log(msg);
809
+ const cwd = process.cwd();
810
+ const root = findRoot(cwd);
811
+ if (!root) {
812
+ console.error("Not a Next.js project (no package.json with next dependency found from " + cwd + ")");
813
+ process.exit(1);
814
+ }
815
+ const appDir = import_path.default.join(root, "src/app");
816
+ if (!import_fs.default.existsSync(appDir)) {
817
+ console.error("Expected src/app directory not found. Use a Next.js app with src directory (e.g. create-next-app --src-dir).");
818
+ process.exit(1);
819
+ }
820
+ log("Infuro CMS init @ " + root);
821
+ if (opts.dryRun) log("(dry run)");
822
+ for (const [filePath, content] of Object.entries(TEMPLATES)) {
823
+ writeFile(root, filePath, content, opts.force, opts.dryRun, log);
824
+ }
825
+ if (!opts.noPatchConfig) {
826
+ log("Config patches:");
827
+ patchNextConfig(root, opts.dryRun, log);
828
+ patchTailwind(root, opts.dryRun, log);
829
+ patchLayout(root, opts.dryRun, log);
830
+ patchPackageJson(root, opts.dryRun, log);
831
+ }
832
+ if (!opts.noDeps && !opts.dryRun) {
833
+ log("Dependencies:");
834
+ await runNpmInstall(root, log);
835
+ } else if (!opts.noDeps && opts.dryRun) {
836
+ log(" would run: npm install @infuro/cms-core typeorm reflect-metadata bcryptjs next-auth next-themes sonner");
837
+ log(" would run: npm install -D tsx dotenv @types/node");
838
+ }
839
+ log("");
840
+ log("Done. Next steps:");
841
+ log(" 1. Copy .env.example to .env and set DATABASE_URL, NEXTAUTH_SECRET, NEXTAUTH_URL, ADMIN_EMAIL, ADMIN_PASSWORD");
842
+ log(" 2. Run npm run migration:run then npm run seed (creates admin from ADMIN_EMAIL/ADMIN_PASSWORD)");
843
+ log(" 3. npm run dev");
844
+ }
845
+ var args = process.argv.slice(2);
846
+ var force = args.includes("--force");
847
+ var dryRun = args.includes("--dry-run");
848
+ var noDeps = args.includes("--no-deps");
849
+ var noPatchConfig = args.includes("--no-patch-config");
850
+ if (args[0] === "init" || args.includes("--init") || args.length === 0 && !args.some((a) => a.startsWith("--"))) {
851
+ runInit({ force, dryRun, noDeps, noPatchConfig }).catch((e) => {
852
+ console.error(e);
853
+ process.exit(1);
854
+ });
855
+ } else {
856
+ console.log("Usage: npx @infuro/cms-core init [--force] [--dry-run] [--no-deps] [--no-patch-config]");
857
+ process.exit(args[0] === "--help" || args[0] === "-h" ? 0 : 1);
858
+ }
859
+ //# sourceMappingURL=cli.cjs.map