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.
- package/dist/cli/index.js +557 -54
- 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
|
-
|
|
8
|
-
|
|
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
|
|
13
|
-
// logo: "/images/logo.png",
|
|
38
|
+
name: "My Blog",
|
|
14
39
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// strings: { ... },
|
|
40
|
+
contentTypes: [articles],
|
|
41
|
+
categories: ["News", "Tutorial", "Opinion"],
|
|
18
42
|
});
|
|
19
43
|
`,
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
430
|
+
p.log.warn(`skip ${relativePath} (already exists)`);
|
|
223
431
|
skipped++;
|
|
224
432
|
continue;
|
|
225
433
|
}
|
|
226
|
-
fs.mkdirSync(
|
|
434
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
227
435
|
fs.writeFileSync(fullPath, content, "utf-8");
|
|
228
|
-
|
|
436
|
+
p.log.success(`create ${relativePath}`);
|
|
229
437
|
created++;
|
|
230
438
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
55
|
-
"react
|
|
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
|
-
"
|
|
60
|
-
"
|
|
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": "^
|
|
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
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
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
|
},
|