gavaengine 2.2.0 → 2.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +557 -54
  2. package/package.json +15 -14
package/dist/cli/index.js CHANGED
@@ -2,22 +2,46 @@
2
2
  #!/usr/bin/env node
3
3
 
4
4
  // src/cli/index.ts
5
+ import * as p from "@clack/prompts";
5
6
  import fs from "fs";
6
7
  import path from "path";
7
- var TEMPLATES = {
8
- "gavaengine.config.ts": `import { defineConfig } from "gavaengine";
8
+
9
+ // src/cli/templates.ts
10
+ var templates = {
11
+ blog: {
12
+ name: "Blog",
13
+ description: "Articles + pages (similar to CronacheMeucci)",
14
+ files: {
15
+ "gavaengine.config.ts": `import { defineConfig, defineContentType } from "gavaengine";
16
+
17
+ const articles = defineContentType({
18
+ slug: "articles",
19
+ labels: { singular: "Article", plural: "Articles" },
20
+ fields: [
21
+ { name: "title", type: "text", label: "Title", placeholder: "Article title" },
22
+ { name: "slug", type: "slug", label: "Slug", generateFrom: "title", admin: { position: "sidebar" } },
23
+ { name: "excerpt", type: "text", label: "Excerpt", placeholder: "Brief description...", admin: { position: "sidebar" } },
24
+ { name: "content", type: "richtext", label: "Content" },
25
+ { name: "coverImage", type: "image", label: "Cover image", admin: { position: "sidebar" } },
26
+ { name: "category", type: "select", label: "Category", admin: { position: "sidebar" } },
27
+ { name: "authorName", type: "text", label: "Author", admin: { position: "sidebar" } },
28
+ ],
29
+ features: { revisions: true, views: true, search: true },
30
+ admin: {
31
+ listColumns: ["title", "category", "status", "updatedAt"],
32
+ searchableFields: ["title", "excerpt", "authorName"],
33
+ },
34
+ });
9
35
 
10
36
  export const geConfig = defineConfig({
11
37
  branding: {
12
- name: "My CMS",
13
- // logo: "/images/logo.png",
38
+ name: "My Blog",
14
39
  },
15
- // categories: ["News", "Blog", "Events"],
16
- // routes: { ... },
17
- // strings: { ... },
40
+ contentTypes: [articles],
41
+ categories: ["News", "Tutorial", "Opinion"],
18
42
  });
19
43
  `,
20
- "src/lib/gava-actions.ts": `"use server";
44
+ "src/lib/gava-actions.ts": `"use server";
21
45
 
22
46
  import { auth } from "@/lib/auth";
23
47
  import { prisma } from "@/lib/prisma";
@@ -47,7 +71,6 @@ async function requireRole(check: (role: string) => boolean) {
47
71
  return session;
48
72
  }
49
73
 
50
- // \u2500\u2500 Articles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
51
74
  export async function getArticles() {
52
75
  await requireAuth();
53
76
  return articles.getArticles();
@@ -63,21 +86,9 @@ export async function createArticle() {
63
86
  return articles.createArticle(session.user.name ?? "");
64
87
  }
65
88
 
66
- export async function updateArticle(
67
- id: string,
68
- data: {
69
- title?: string;
70
- slug?: string;
71
- excerpt?: string;
72
- content?: string;
73
- coverImage?: string;
74
- category?: string;
75
- authorName?: string;
76
- }
77
- ) {
89
+ export async function updateArticle(id: string, data: Record<string, any>) {
78
90
  const session = await requireRole(geConfig.roles.canEdit);
79
- const result = await articles.updateArticle(session.user.id, id, data);
80
- return result;
91
+ return articles.updateArticle(session.user.id, id, data);
81
92
  }
82
93
 
83
94
  export async function deleteArticle(id: string) {
@@ -101,7 +112,6 @@ export async function unpublishArticle(id: string) {
101
112
  return result;
102
113
  }
103
114
 
104
- // \u2500\u2500 Media \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
105
115
  export async function getMedia(search?: string) {
106
116
  await requireAuth();
107
117
  return media.getMedia(search);
@@ -114,7 +124,6 @@ export async function deleteMedia(id: string) {
114
124
  return result;
115
125
  }
116
126
 
117
- // \u2500\u2500 Revisions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
118
127
  export async function getRevisions(articleId: string) {
119
128
  await requireAuth();
120
129
  return revisions.getRevisions(articleId);
@@ -125,7 +134,6 @@ export async function restoreRevision(articleId: string, revisionId: string) {
125
134
  return revisions.restoreRevision(articleId, revisionId, session.user.id);
126
135
  }
127
136
 
128
- // \u2500\u2500 Users \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
129
137
  export async function getUsers() {
130
138
  const session = await requireAuth();
131
139
  if (session.user.role !== geConfig.roles.adminRole) throw new Error("Unauthorized");
@@ -156,7 +164,7 @@ export async function deleteUser(id: string) {
156
164
  return users.deleteUser(session.user.id, id);
157
165
  }
158
166
  `,
159
- "src/app/api/upload/route.ts": `import { NextRequest, NextResponse } from "next/server";
167
+ "src/app/api/upload/route.ts": `import { NextRequest, NextResponse } from "next/server";
160
168
  import { auth } from "@/lib/auth";
161
169
  import { prisma } from "@/lib/prisma";
162
170
  import { geConfig } from "@/gavaengine.config";
@@ -196,46 +204,541 @@ export async function POST(request: NextRequest) {
196
204
  return NextResponse.json({ url: result.url });
197
205
  }
198
206
  `
199
- };
200
- function main() {
201
- const args = process.argv.slice(2);
202
- const command = args[0];
203
- if (command !== "init") {
204
- console.log("Usage: npx gavaengine init");
205
- console.log("");
206
- console.log("Commands:");
207
- console.log(" init Scaffold GavaEngine files in your project");
208
- process.exit(0);
207
+ }
208
+ },
209
+ portfolio: {
210
+ name: "Portfolio",
211
+ description: "Projects + pages for creative portfolios",
212
+ files: {
213
+ "gavaengine.config.ts": `import { defineConfig, defineContentType } from "gavaengine";
214
+
215
+ const projects = defineContentType({
216
+ slug: "projects",
217
+ labels: { singular: "Project", plural: "Projects" },
218
+ fields: [
219
+ { name: "title", type: "text", label: "Title" },
220
+ { name: "slug", type: "slug", label: "Slug", generateFrom: "title", admin: { position: "sidebar" } },
221
+ { name: "description", type: "text", label: "Description" },
222
+ { name: "content", type: "richtext", label: "Content" },
223
+ { name: "coverImage", type: "image", label: "Cover image", admin: { position: "sidebar" } },
224
+ { name: "category", type: "select", label: "Category", options: ["Web", "Mobile", "Design", "Other"], admin: { position: "sidebar" } },
225
+ { name: "url", type: "text", label: "Project URL", admin: { position: "sidebar" } },
226
+ { name: "year", type: "number", label: "Year", admin: { position: "sidebar" } },
227
+ ],
228
+ features: { revisions: true, views: true },
229
+ admin: {
230
+ listColumns: ["title", "category", "year", "status"],
231
+ searchableFields: ["title", "description"],
232
+ },
233
+ });
234
+
235
+ export const geConfig = defineConfig({
236
+ branding: {
237
+ name: "My Portfolio",
238
+ },
239
+ contentTypes: [projects],
240
+ categories: ["Web", "Mobile", "Design", "Other"],
241
+ });
242
+ `
243
+ }
244
+ },
245
+ docs: {
246
+ name: "Documentation",
247
+ description: "Documentation site with hierarchy",
248
+ files: {
249
+ "gavaengine.config.ts": `import { defineConfig, defineContentType } from "gavaengine";
250
+
251
+ const docs = defineContentType({
252
+ slug: "docs",
253
+ labels: { singular: "Document", plural: "Documentation" },
254
+ fields: [
255
+ { name: "title", type: "text", label: "Title" },
256
+ { name: "slug", type: "slug", label: "Slug", generateFrom: "title", admin: { position: "sidebar" } },
257
+ { name: "content", type: "richtext", label: "Content" },
258
+ { name: "category", type: "select", label: "Section", admin: { position: "sidebar" } },
259
+ { name: "sortOrder", type: "number", label: "Sort order", admin: { position: "sidebar" } },
260
+ ],
261
+ statuses: ["draft", "published"],
262
+ features: { revisions: true, search: true },
263
+ admin: {
264
+ listColumns: ["title", "category", "sortOrder", "status"],
265
+ searchableFields: ["title", "content"],
266
+ defaultSort: { field: "sortOrder", direction: "asc" },
267
+ },
268
+ });
269
+
270
+ export const geConfig = defineConfig({
271
+ branding: {
272
+ name: "My Docs",
273
+ },
274
+ contentTypes: [docs],
275
+ categories: ["Getting Started", "Guides", "API Reference", "FAQ"],
276
+ });
277
+ `
278
+ }
279
+ },
280
+ blank: {
281
+ name: "Blank",
282
+ description: "Minimal setup \u2014 configure everything yourself",
283
+ files: {
284
+ "gavaengine.config.ts": `import { defineConfig } from "gavaengine";
285
+
286
+ export const geConfig = defineConfig({
287
+ branding: {
288
+ name: "My CMS",
289
+ // logo: "/images/logo.png",
290
+ },
291
+ // contentTypes: [],
292
+ // categories: [],
293
+ // locale: "en",
294
+ });
295
+ `
296
+ }
209
297
  }
210
- const cwd = process.cwd();
298
+ };
299
+
300
+ // src/cli/index.ts
301
+ var VERSION = "2.3.0";
302
+ function banner() {
211
303
  console.log("");
212
304
  console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
213
- console.log(" \u2551 GAVA ENGINE init \u2551");
305
+ console.log(" \u2551 GAVA ENGINE CLI \u2551");
214
306
  console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
215
307
  console.log("");
308
+ }
309
+ async function cmdCreate(projectName) {
310
+ p.intro("gavaengine create");
311
+ if (!projectName) {
312
+ const name = await p.text({
313
+ message: "Project name:",
314
+ placeholder: "my-blog",
315
+ validate: (v) => {
316
+ if (!v.trim()) return "Project name is required";
317
+ if (/[^a-zA-Z0-9._-]/.test(v)) return "Invalid characters in name";
318
+ }
319
+ });
320
+ if (p.isCancel(name)) return p.cancel("Cancelled.");
321
+ projectName = name;
322
+ }
323
+ const dest = path.resolve(process.cwd(), projectName);
324
+ if (fs.existsSync(dest)) {
325
+ p.cancel(`Directory "${projectName}" already exists.`);
326
+ return;
327
+ }
328
+ const templateKey = await p.select({
329
+ message: "Choose a template:",
330
+ options: Object.entries(templates).map(([key, t]) => ({
331
+ value: key,
332
+ label: t.name,
333
+ hint: t.description
334
+ }))
335
+ });
336
+ if (p.isCancel(templateKey)) return p.cancel("Cancelled.");
337
+ const template = templates[templateKey];
338
+ const s = p.spinner();
339
+ s.start("Creating project...");
340
+ fs.mkdirSync(dest, { recursive: true });
341
+ let created = 0;
342
+ for (const [relativePath, content] of Object.entries(template.files)) {
343
+ const fullPath = path.join(dest, relativePath);
344
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
345
+ fs.writeFileSync(fullPath, content, "utf-8");
346
+ created++;
347
+ }
348
+ const pkg = {
349
+ name: projectName,
350
+ version: "0.1.0",
351
+ private: true,
352
+ scripts: {
353
+ dev: "next dev --turbopack",
354
+ build: "next build",
355
+ start: "next start"
356
+ },
357
+ dependencies: {
358
+ gavaengine: `^${VERSION}`,
359
+ next: "^16.0.0",
360
+ react: "^19.0.0",
361
+ "react-dom": "^19.0.0",
362
+ "@tiptap/react": "^3.0.0",
363
+ "@tiptap/starter-kit": "^3.0.0",
364
+ "@tiptap/pm": "^3.0.0",
365
+ "lucide-react": "^0.500.0",
366
+ "next-themes": "^0.4.0"
367
+ }
368
+ };
369
+ fs.writeFileSync(
370
+ path.join(dest, "package.json"),
371
+ JSON.stringify(pkg, null, 2) + "\n",
372
+ "utf-8"
373
+ );
374
+ const tsconfig = {
375
+ compilerOptions: {
376
+ target: "ES2017",
377
+ lib: ["dom", "dom.iterable", "esnext"],
378
+ allowJs: true,
379
+ skipLibCheck: true,
380
+ strict: true,
381
+ noEmit: true,
382
+ esModuleInterop: true,
383
+ module: "esnext",
384
+ moduleResolution: "bundler",
385
+ resolveJsonModule: true,
386
+ isolatedModules: true,
387
+ jsx: "preserve",
388
+ incremental: true,
389
+ paths: { "@/*": ["./src/*"] }
390
+ },
391
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
392
+ exclude: ["node_modules"]
393
+ };
394
+ fs.writeFileSync(
395
+ path.join(dest, "tsconfig.json"),
396
+ JSON.stringify(tsconfig, null, 2) + "\n",
397
+ "utf-8"
398
+ );
399
+ s.stop(`Created ${created + 2} files.`);
400
+ p.note(
401
+ [
402
+ `cd ${projectName}`,
403
+ "npm install",
404
+ "# Copy Prisma models from node_modules/gavaengine/src/prisma/schema.partial.prisma",
405
+ "# Configure your auth (see gavaengine docs)",
406
+ "npm run dev"
407
+ ].join("\n"),
408
+ "Next steps"
409
+ );
410
+ p.outro("Happy building!");
411
+ }
412
+ async function cmdInit() {
413
+ p.intro("gavaengine init");
414
+ const cwd = process.cwd();
415
+ const templateKey = await p.select({
416
+ message: "Choose a template:",
417
+ options: Object.entries(templates).map(([key, t]) => ({
418
+ value: key,
419
+ label: t.name,
420
+ hint: t.description
421
+ }))
422
+ });
423
+ if (p.isCancel(templateKey)) return p.cancel("Cancelled.");
424
+ const template = templates[templateKey];
216
425
  let created = 0;
217
426
  let skipped = 0;
218
- for (const [relativePath, content] of Object.entries(TEMPLATES)) {
427
+ for (const [relativePath, content] of Object.entries(template.files)) {
219
428
  const fullPath = path.join(cwd, relativePath);
220
- const dir = path.dirname(fullPath);
221
429
  if (fs.existsSync(fullPath)) {
222
- console.log(` skip ${relativePath} (already exists)`);
430
+ p.log.warn(`skip ${relativePath} (already exists)`);
223
431
  skipped++;
224
432
  continue;
225
433
  }
226
- fs.mkdirSync(dir, { recursive: true });
434
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
227
435
  fs.writeFileSync(fullPath, content, "utf-8");
228
- console.log(` create ${relativePath}`);
436
+ p.log.success(`create ${relativePath}`);
229
437
  created++;
230
438
  }
231
- console.log("");
232
- console.log(` Done! ${created} files created, ${skipped} skipped.`);
233
- console.log("");
234
- console.log(" Next steps:");
235
- console.log(" 1. Copy models from node_modules/gavaengine/src/prisma/schema.partial.prisma into your schema");
236
- console.log(" 2. Run prisma migrate dev to create tables");
237
- console.log(" 3. Import 'gavaengine/styles' in your layout");
238
- console.log(" 4. Wrap your dashboard with <GavaEngineProvider>");
239
- console.log("");
439
+ p.note(
440
+ [
441
+ "Copy models from node_modules/gavaengine/src/prisma/schema.partial.prisma",
442
+ "Run prisma migrate dev to create tables",
443
+ "Import 'gavaengine/styles' in your layout",
444
+ "Wrap your dashboard with <GavaEngineProvider>"
445
+ ].join("\n"),
446
+ "Next steps"
447
+ );
448
+ p.outro(`Done! ${created} created, ${skipped} skipped.`);
449
+ }
450
+ async function cmdGenerate() {
451
+ p.intro("gavaengine generate");
452
+ const cwd = process.cwd();
453
+ const configPath = path.join(cwd, "gavaengine.config.ts");
454
+ if (!fs.existsSync(configPath)) {
455
+ p.cancel("No gavaengine.config.ts found. Run 'npx gavaengine init' first.");
456
+ return;
457
+ }
458
+ const s = p.spinner();
459
+ s.start("Reading config...");
460
+ const partialSchemaPath = path.join(
461
+ cwd,
462
+ "node_modules",
463
+ "gavaengine",
464
+ "src",
465
+ "prisma",
466
+ "schema.partial.prisma"
467
+ );
468
+ if (!fs.existsSync(partialSchemaPath)) {
469
+ s.stop("Error");
470
+ p.cancel(
471
+ "Could not find schema.partial.prisma. Make sure gavaengine is installed."
472
+ );
473
+ return;
474
+ }
475
+ const partialSchema = fs.readFileSync(partialSchemaPath, "utf-8");
476
+ const prismaDir = path.join(cwd, "prisma");
477
+ fs.mkdirSync(prismaDir, { recursive: true });
478
+ const targetFile = path.join(prismaDir, "gavaengine.prisma");
479
+ fs.writeFileSync(targetFile, partialSchema, "utf-8");
480
+ s.stop("Done");
481
+ p.log.success(`Wrote ${path.relative(cwd, targetFile)}`);
482
+ p.note(
483
+ "Include this file in your Prisma schema or copy the models into your main schema.",
484
+ "Prisma setup"
485
+ );
486
+ p.outro("Schema generated!");
240
487
  }
241
- main();
488
+ async function cmdAddLocale(code) {
489
+ p.intro("gavaengine add-locale");
490
+ if (!code) {
491
+ const input = await p.text({
492
+ message: "Locale code (e.g. es, fr, de):",
493
+ placeholder: "es",
494
+ validate: (v) => {
495
+ if (!v.trim()) return "Locale code is required";
496
+ if (!/^[a-z]{2}(-[A-Z]{2})?$/.test(v.trim()))
497
+ return "Use format: xx or xx-XX";
498
+ }
499
+ });
500
+ if (p.isCancel(input)) return p.cancel("Cancelled.");
501
+ code = input;
502
+ }
503
+ const cwd = process.cwd();
504
+ const filePath = path.join(cwd, "src", "i18n", `${code}.ts`);
505
+ if (fs.existsSync(filePath)) {
506
+ p.cancel(`File already exists: ${path.relative(cwd, filePath)}`);
507
+ return;
508
+ }
509
+ const content = `import type { GELocale } from "gavaengine/i18n";
510
+
511
+ export const ${code.replace("-", "_")}: GELocale = {
512
+ content: {
513
+ articles: "Articles",
514
+ newArticle: "New article",
515
+ editArticle: "Edit article",
516
+ deleteConfirm: "Delete this article?",
517
+ noArticles: "No articles yet",
518
+ title: "Title",
519
+ slug: "Slug",
520
+ excerpt: "Excerpt",
521
+ content: "Content",
522
+ coverImage: "Cover image",
523
+ category: "Category",
524
+ author: "Author",
525
+ status: "Status",
526
+ draft: "Draft",
527
+ published: "Published",
528
+ publishedAt: "Published at",
529
+ createdAt: "Created at",
530
+ updatedAt: "Updated at",
531
+ views: "Views",
532
+ actions: "Actions",
533
+ },
534
+ editor: {
535
+ placeholder: "Start writing...",
536
+ bold: "Bold",
537
+ italic: "Italic",
538
+ underline: "Underline",
539
+ strikethrough: "Strikethrough",
540
+ heading1: "Heading 1",
541
+ heading2: "Heading 2",
542
+ heading3: "Heading 3",
543
+ bulletList: "Bullet list",
544
+ orderedList: "Ordered list",
545
+ blockquote: "Blockquote",
546
+ codeBlock: "Code block",
547
+ link: "Link",
548
+ image: "Image",
549
+ undo: "Undo",
550
+ redo: "Redo",
551
+ alignLeft: "Align left",
552
+ alignCenter: "Align center",
553
+ alignRight: "Align right",
554
+ table: "Table",
555
+ youtube: "YouTube",
556
+ video: "Video",
557
+ subscript: "Subscript",
558
+ superscript: "Superscript",
559
+ highlight: "Highlight",
560
+ color: "Color",
561
+ save: "Save",
562
+ publish: "Publish",
563
+ unpublish: "Unpublish",
564
+ delete: "Delete",
565
+ revisions: "Revisions",
566
+ restoreRevision: "Restore revision",
567
+ },
568
+ media: {
569
+ title: "Media",
570
+ upload: "Upload",
571
+ search: "Search media...",
572
+ noMedia: "No media yet",
573
+ deleteConfirm: "Delete this file?",
574
+ dropzone: "Drop files here or click to upload",
575
+ pickImage: "Pick image",
576
+ },
577
+ users: {
578
+ title: "Users",
579
+ newUser: "New user",
580
+ editUser: "Edit user",
581
+ deleteConfirm: "Delete this user?",
582
+ name: "Name",
583
+ email: "Email",
584
+ role: "Role",
585
+ password: "Password",
586
+ },
587
+ common: {
588
+ save: "Save",
589
+ cancel: "Cancel",
590
+ delete: "Delete",
591
+ edit: "Edit",
592
+ create: "Create",
593
+ search: "Search...",
594
+ loading: "Loading...",
595
+ error: "Error",
596
+ success: "Success",
597
+ confirm: "Confirm",
598
+ back: "Back",
599
+ noResults: "No results",
600
+ dashboard: "Dashboard",
601
+ settings: "Settings",
602
+ logout: "Logout",
603
+ },
604
+ };
605
+ `;
606
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
607
+ fs.writeFileSync(filePath, content, "utf-8");
608
+ p.log.success(`Created ${path.relative(cwd, filePath)}`);
609
+ p.note(
610
+ [
611
+ "1. Translate the strings in the generated file",
612
+ `2. Register it: registerLocale("${code}", ${code.replace("-", "_")})`,
613
+ `3. Use it: defineConfig({ locale: "${code}" })`
614
+ ].join("\n"),
615
+ "Next steps"
616
+ );
617
+ p.outro("Locale scaffolded!");
618
+ }
619
+ async function cmdAddContentType() {
620
+ p.intro("gavaengine add-content-type");
621
+ const name = await p.text({
622
+ message: "Content type name (plural, e.g. 'events'):",
623
+ placeholder: "events",
624
+ validate: (v) => {
625
+ if (!v.trim()) return "Name is required";
626
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(v.trim()))
627
+ return "Use camelCase starting with lowercase";
628
+ }
629
+ });
630
+ if (p.isCancel(name)) return p.cancel("Cancelled.");
631
+ const singular = await p.text({
632
+ message: "Singular label (e.g. 'Event'):",
633
+ placeholder: name.charAt(0).toUpperCase() + name.slice(1, -1)
634
+ });
635
+ if (p.isCancel(singular)) return p.cancel("Cancelled.");
636
+ const plural = await p.text({
637
+ message: "Plural label (e.g. 'Events'):",
638
+ placeholder: name.charAt(0).toUpperCase() + name.slice(1)
639
+ });
640
+ if (p.isCancel(plural)) return p.cancel("Cancelled.");
641
+ const features = await p.multiselect({
642
+ message: "Enable features:",
643
+ options: [
644
+ { value: "revisions", label: "Revisions", hint: "Track content history" },
645
+ { value: "views", label: "View counter", hint: "Count page views" },
646
+ { value: "search", label: "Full-text search" }
647
+ ],
648
+ required: false
649
+ });
650
+ if (p.isCancel(features)) return p.cancel("Cancelled.");
651
+ const featuresObj = {};
652
+ for (const f of features) {
653
+ featuresObj[f] = true;
654
+ }
655
+ const code = `import { defineContentType } from "gavaengine";
656
+
657
+ export const ${name} = defineContentType({
658
+ slug: "${name}",
659
+ labels: { singular: "${singular}", plural: "${plural}" },
660
+ fields: [
661
+ { name: "title", type: "text", label: "Title", placeholder: "${singular} title" },
662
+ { name: "slug", type: "slug", label: "Slug", generateFrom: "title", admin: { position: "sidebar" } },
663
+ { name: "content", type: "richtext", label: "Content" },
664
+ { name: "coverImage", type: "image", label: "Cover image", admin: { position: "sidebar" } },
665
+ { name: "category", type: "select", label: "Category", admin: { position: "sidebar" } },
666
+ ],
667
+ features: ${JSON.stringify(featuresObj)},
668
+ admin: {
669
+ listColumns: ["title", "category", "status", "updatedAt"],
670
+ searchableFields: ["title"],
671
+ },
672
+ });
673
+ `;
674
+ const cwd = process.cwd();
675
+ const filePath = path.join(cwd, "src", "content", `${name}.ts`);
676
+ if (fs.existsSync(filePath)) {
677
+ p.cancel(`File already exists: ${path.relative(cwd, filePath)}`);
678
+ return;
679
+ }
680
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
681
+ fs.writeFileSync(filePath, code, "utf-8");
682
+ p.log.success(`Created ${path.relative(cwd, filePath)}`);
683
+ p.note(
684
+ [
685
+ `1. Customize fields in ${path.relative(cwd, filePath)}`,
686
+ `2. Import in gavaengine.config.ts: import { ${name} } from "./src/content/${name}"`,
687
+ `3. Add to contentTypes: [${name}]`,
688
+ "4. Run npx gavaengine generate to update Prisma schema"
689
+ ].join("\n"),
690
+ "Next steps"
691
+ );
692
+ p.outro("Content type scaffolded!");
693
+ }
694
+ async function main() {
695
+ const args = process.argv.slice(2);
696
+ const command = args[0];
697
+ const arg1 = args[1];
698
+ if (command === "--version" || command === "-v") {
699
+ console.log(`gavaengine v${VERSION}`);
700
+ return;
701
+ }
702
+ if (command === "--help" || command === "-h" || !command) {
703
+ banner();
704
+ console.log(` gavaengine v${VERSION}`);
705
+ console.log("");
706
+ console.log(" Commands:");
707
+ console.log(" create [name] Create a new project from template");
708
+ console.log(" init Setup GavaEngine in existing project");
709
+ console.log(" generate Generate Prisma schema from config");
710
+ console.log(" add-locale [code] Scaffold a new locale file");
711
+ console.log(" add-content-type Scaffold a new content type");
712
+ console.log("");
713
+ console.log(" Options:");
714
+ console.log(" -v, --version Show version");
715
+ console.log(" -h, --help Show help");
716
+ console.log("");
717
+ return;
718
+ }
719
+ switch (command) {
720
+ case "create":
721
+ await cmdCreate(arg1);
722
+ break;
723
+ case "init":
724
+ await cmdInit();
725
+ break;
726
+ case "generate":
727
+ await cmdGenerate();
728
+ break;
729
+ case "add-locale":
730
+ await cmdAddLocale(arg1);
731
+ break;
732
+ case "add-content-type":
733
+ await cmdAddContentType();
734
+ break;
735
+ default:
736
+ console.error(`Unknown command: ${command}`);
737
+ console.error('Run "npx gavaengine --help" for usage.');
738
+ process.exit(1);
739
+ }
740
+ }
741
+ main().catch((err) => {
742
+ console.error(err);
743
+ process.exit(1);
744
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gavaengine",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Modular CMS engine — editor, auth, media & user management as React components",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,15 +51,15 @@
51
51
  "gavaengine": "./dist/cli/index.js"
52
52
  },
53
53
  "peerDependencies": {
54
- "react": "^18.0.0 || ^19.0.0",
55
- "react-dom": "^18.0.0 || ^19.0.0",
54
+ "@tiptap/pm": "^3.0.0",
55
+ "@tiptap/react": "^3.0.0",
56
+ "@tiptap/starter-kit": "^3.0.0",
57
+ "lucide-react": ">=0.300.0",
56
58
  "next": ">=14.0.0",
57
59
  "next-auth": "^5.0.0-beta.0",
58
60
  "next-themes": ">=0.3.0",
59
- "@tiptap/react": "^3.0.0",
60
- "@tiptap/starter-kit": "^3.0.0",
61
- "@tiptap/pm": "^3.0.0",
62
- "lucide-react": ">=0.300.0"
61
+ "react": "^18.0.0 || ^19.0.0",
62
+ "react-dom": "^18.0.0 || ^19.0.0"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "next-auth": {
@@ -70,6 +70,7 @@
70
70
  }
71
71
  },
72
72
  "dependencies": {
73
+ "@clack/prompts": "^1.0.0",
73
74
  "@tiptap/extension-color": "^3.19.0",
74
75
  "@tiptap/extension-highlight": "^3.19.0",
75
76
  "@tiptap/extension-image": "^3.19.0",
@@ -88,20 +89,20 @@
88
89
  "bcryptjs": "^3.0.0"
89
90
  },
90
91
  "devDependencies": {
92
+ "@tiptap/pm": "^3.19.0",
93
+ "@tiptap/react": "^3.19.0",
94
+ "@tiptap/starter-kit": "^3.19.0",
91
95
  "@types/bcryptjs": "^2.4.6",
92
96
  "@types/node": "^20",
93
97
  "@types/react": "^19",
94
98
  "@types/react-dom": "^19",
95
- "react": "^19.0.0",
96
- "react-dom": "^19.0.0",
97
- "react-easy-crop": "^5.5.6",
99
+ "lucide-react": "^0.563.0",
98
100
  "next": "^16.0.0",
99
101
  "next-auth": "^5.0.0-beta.30",
100
102
  "next-themes": "^0.4.0",
101
- "@tiptap/react": "^3.19.0",
102
- "@tiptap/starter-kit": "^3.19.0",
103
- "@tiptap/pm": "^3.19.0",
104
- "lucide-react": "^0.563.0",
103
+ "react": "^19.0.0",
104
+ "react-dom": "^19.0.0",
105
+ "react-easy-crop": "^5.5.6",
105
106
  "tsup": "^8.0.0",
106
107
  "typescript": "^5.0.0"
107
108
  },