create-kide-app 0.0.8 → 0.0.9
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/index.js +142 -50
- package/package.json +1 -1
- package/templates/base/.env.example +2 -0
- package/templates/base/package.json +39 -0
- package/templates/base/src/cms/adapters/db.ts +49 -0
- package/templates/base/src/cms/adapters/email.ts +34 -0
- package/templates/base/src/cms/adapters/storage.ts +29 -0
- package/templates/base/src/cms/cms.config.ts +7 -0
- package/templates/base/src/cms/collections/users.ts +20 -0
- package/templates/base/src/cms/internals/create-admin.ts +40 -0
- package/templates/base/src/cms/internals/generator.ts +10 -0
- package/templates/base/src/cms/internals/runtime.ts +76 -0
- package/templates/base/src/env.d.ts +12 -0
- package/templates/base/src/pages/index.astro +11 -0
- package/templates/base/src/styles/admin.css +253 -0
- package/templates/base/src/styles/public.css +2 -0
- package/templates/base/tsconfig.json +11 -0
- package/templates/cloudflare/astro.config.mjs +1 -1
- package/templates/cloudflare/uploads-route.ts +1 -1
- package/templates/demo/src/cms/cms.config.ts +17 -0
- package/templates/demo/src/cms/collections/authors.ts +20 -0
- package/templates/demo/src/cms/collections/front-page.ts +37 -0
- package/templates/demo/src/cms/collections/menus.ts +18 -0
- package/templates/demo/src/cms/collections/pages.ts +59 -0
- package/templates/demo/src/cms/collections/posts.ts +55 -0
- package/templates/demo/src/cms/collections/taxonomies.ts +18 -0
- package/templates/demo/src/cms/collections/users.ts +20 -0
- package/templates/demo/src/cms/internals/seed.data.ts +506 -0
- package/templates/demo/src/cms/internals/seed.ts +7 -0
- package/templates/demo/src/components/BlockRenderer.astro +32 -0
- package/templates/demo/src/components/RichTextContent.astro +18 -0
- package/templates/demo/src/components/blocks/Faq.astro +22 -0
- package/templates/demo/src/components/blocks/Hero.astro +16 -0
- package/templates/demo/src/components/blocks/Image.astro +25 -0
- package/templates/demo/src/components/blocks/Text.astro +16 -0
- package/templates/demo/src/layouts/PublicLayout.astro +54 -0
- package/templates/demo/src/pages/[...slug].astro +31 -0
- package/templates/demo/src/pages/blog/[slug].astro +41 -0
- package/templates/demo/src/pages/index.astro +51 -0
- package/templates/demo/src/scripts/preview.ts +33 -0
- package/templates/local/astro.config.mjs +1 -1
package/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
-
import { cpSync, existsSync, mkdirSync, readFileSync,
|
|
5
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
@@ -13,9 +13,6 @@ const TEMPLATES_DIR = path.join(__dirname, "templates");
|
|
|
13
13
|
|
|
14
14
|
const pm = { name: "pnpm", exec: "pnpm dlx", run: "pnpm", install: "pnpm install" };
|
|
15
15
|
|
|
16
|
-
// --- Template repo URL ---
|
|
17
|
-
const REPO_URL = "https://github.com/mhernesniemi/kide-cms/archive/refs/heads/main.tar.gz";
|
|
18
|
-
|
|
19
16
|
// --- Main ---
|
|
20
17
|
|
|
21
18
|
async function main() {
|
|
@@ -74,36 +71,15 @@ async function main() {
|
|
|
74
71
|
|
|
75
72
|
const s = p.spinner();
|
|
76
73
|
|
|
77
|
-
// --- Scaffold ---
|
|
74
|
+
// --- Scaffold from base template ---
|
|
78
75
|
|
|
79
76
|
s.start(`Scaffolding project (using ${pm.name})`);
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
const tmpArchive = path.join(projectDir, "_template.tar.gz");
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
execSync(`curl -sL "${REPO_URL}" -o "${tmpArchive}"`, { stdio: "pipe" });
|
|
86
|
-
execSync(`tar -xzf "${tmpArchive}" -C "${projectDir}" --strip-components=1`, { stdio: "pipe" });
|
|
87
|
-
rmSync(tmpArchive, { force: true });
|
|
88
|
-
} catch {
|
|
89
|
-
s.message("Archive download failed, trying git clone...");
|
|
90
|
-
rmSync(projectDir, { recursive: true, force: true });
|
|
91
|
-
try {
|
|
92
|
-
execSync(`git clone --depth 1 https://github.com/mhernesniemi/kide-cms.git "${projectDir}"`, {
|
|
93
|
-
stdio: "pipe",
|
|
94
|
-
});
|
|
95
|
-
rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
|
|
96
|
-
} catch {
|
|
97
|
-
s.stop("Failed to download template.");
|
|
98
|
-
p.cancel("Check your network connection.");
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
78
|
+
cpSync(path.join(TEMPLATES_DIR, "base"), projectDir, { recursive: true });
|
|
102
79
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (existsSync(fp)) rmSync(fp, { recursive: true, force: true });
|
|
80
|
+
// Apply demo schema and seed data if selected
|
|
81
|
+
if (seedDemo) {
|
|
82
|
+
cpSync(path.join(TEMPLATES_DIR, "demo"), projectDir, { recursive: true });
|
|
107
83
|
}
|
|
108
84
|
|
|
109
85
|
s.stop("Project scaffolded");
|
|
@@ -115,11 +91,11 @@ async function main() {
|
|
|
115
91
|
const targetDir = path.join(TEMPLATES_DIR, target);
|
|
116
92
|
|
|
117
93
|
cpSync(path.join(targetDir, "astro.config.mjs"), path.join(projectDir, "astro.config.mjs"));
|
|
118
|
-
cpSync(path.join(targetDir, "db.ts"), path.join(projectDir, "src/cms/
|
|
94
|
+
cpSync(path.join(targetDir, "db.ts"), path.join(projectDir, "src/cms/adapters/db.ts"));
|
|
119
95
|
cpSync(path.join(targetDir, "drizzle.config.ts"), path.join(projectDir, "drizzle.config.ts"));
|
|
120
96
|
|
|
121
97
|
if (target === "cloudflare") {
|
|
122
|
-
cpSync(path.join(targetDir, "storage.ts"), path.join(projectDir, "src/cms/
|
|
98
|
+
cpSync(path.join(targetDir, "storage.ts"), path.join(projectDir, "src/cms/adapters/storage.ts"));
|
|
123
99
|
const uploadsRouteDir = path.join(projectDir, "src/pages/uploads");
|
|
124
100
|
mkdirSync(uploadsRouteDir, { recursive: true });
|
|
125
101
|
cpSync(path.join(targetDir, "uploads-route.ts"), path.join(uploadsRouteDir, "[...path].ts"));
|
|
@@ -130,6 +106,10 @@ async function main() {
|
|
|
130
106
|
|
|
131
107
|
pkg.name = projectName;
|
|
132
108
|
|
|
109
|
+
if (seedDemo) {
|
|
110
|
+
pkg.scripts["cms:seed"] = "node --import tsx src/cms/internals/seed.ts";
|
|
111
|
+
}
|
|
112
|
+
|
|
133
113
|
if (target === "cloudflare") {
|
|
134
114
|
delete pkg.dependencies["@astrojs/node"];
|
|
135
115
|
pkg.dependencies["@astrojs/cloudflare"] = "^13.0.0";
|
|
@@ -216,6 +196,119 @@ async function main() {
|
|
|
216
196
|
);
|
|
217
197
|
}
|
|
218
198
|
|
|
199
|
+
// --- Cloudflare resource setup ---
|
|
200
|
+
|
|
201
|
+
const cf = { d1Created: false, r2Created: false, migrationsApplied: false };
|
|
202
|
+
if (target === "cloudflare") {
|
|
203
|
+
const setupNow = await p.confirm({
|
|
204
|
+
message: "Set up Cloudflare resources now? (creates D1 database and R2 bucket)",
|
|
205
|
+
initialValue: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!p.isCancel(setupNow) && setupNow) {
|
|
209
|
+
// Check wrangler authentication
|
|
210
|
+
let authenticated = false;
|
|
211
|
+
try {
|
|
212
|
+
execSync(`${pm.exec} wrangler whoami`, { cwd: projectDir, stdio: "pipe" });
|
|
213
|
+
authenticated = true;
|
|
214
|
+
} catch {
|
|
215
|
+
p.note("You need to log in to Cloudflare first.", "Wrangler login required");
|
|
216
|
+
const doLogin = await p.confirm({ message: "Open browser to log in?", initialValue: true });
|
|
217
|
+
if (!p.isCancel(doLogin) && doLogin) {
|
|
218
|
+
try {
|
|
219
|
+
execSync(`${pm.exec} wrangler login`, { cwd: projectDir, stdio: "inherit" });
|
|
220
|
+
authenticated = true;
|
|
221
|
+
} catch {
|
|
222
|
+
s.stop("Login failed");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (authenticated) {
|
|
228
|
+
// Create D1 database
|
|
229
|
+
let databaseId = null;
|
|
230
|
+
s.start("Creating D1 database");
|
|
231
|
+
try {
|
|
232
|
+
const output = execSync(`${pm.exec} wrangler d1 create ${projectName}-db`, {
|
|
233
|
+
cwd: projectDir,
|
|
234
|
+
stdio: "pipe",
|
|
235
|
+
}).toString();
|
|
236
|
+
// Parse database_id from output (looks for UUID-like string)
|
|
237
|
+
const match = output.match(/database_id\s*=\s*"([^"]+)"/);
|
|
238
|
+
if (match) databaseId = match[1];
|
|
239
|
+
cf.d1Created = true;
|
|
240
|
+
s.stop("D1 database created");
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// Already exists — look it up
|
|
243
|
+
try {
|
|
244
|
+
const listOutput = execSync(`${pm.exec} wrangler d1 list`, { cwd: projectDir, stdio: "pipe" }).toString();
|
|
245
|
+
const lines = listOutput.split("\n");
|
|
246
|
+
const dbLine = lines.find((l) => l.includes(`${projectName}-db`));
|
|
247
|
+
if (dbLine) {
|
|
248
|
+
const idMatch = dbLine.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
|
|
249
|
+
if (idMatch) databaseId = idMatch[0];
|
|
250
|
+
}
|
|
251
|
+
if (databaseId) {
|
|
252
|
+
cf.d1Created = true;
|
|
253
|
+
s.stop("D1 database already exists — using existing");
|
|
254
|
+
} else {
|
|
255
|
+
s.stop("D1 setup failed");
|
|
256
|
+
if (err.stderr) console.error(err.stderr.toString());
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
s.stop("D1 setup failed");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Update wrangler.toml with database_id
|
|
264
|
+
if (databaseId) {
|
|
265
|
+
const wranglerPath = path.join(projectDir, "wrangler.toml");
|
|
266
|
+
let wranglerContent = readFileSync(wranglerPath, "utf-8");
|
|
267
|
+
wranglerContent = wranglerContent.replace(
|
|
268
|
+
/database_id = "" #[^\n]*/,
|
|
269
|
+
`database_id = "${databaseId}"`,
|
|
270
|
+
);
|
|
271
|
+
writeFileSync(wranglerPath, wranglerContent);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Create R2 bucket
|
|
275
|
+
s.start("Creating R2 bucket");
|
|
276
|
+
try {
|
|
277
|
+
execSync(`${pm.exec} wrangler r2 bucket create ${projectName}-assets`, { cwd: projectDir, stdio: "pipe" });
|
|
278
|
+
cf.r2Created = true;
|
|
279
|
+
s.stop("R2 bucket created");
|
|
280
|
+
} catch {
|
|
281
|
+
// Already exists is fine — assume it's there
|
|
282
|
+
cf.r2Created = true;
|
|
283
|
+
s.stop("R2 bucket already exists");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Generate migrations and apply to remote D1
|
|
287
|
+
if (databaseId) {
|
|
288
|
+
s.start("Generating database migrations");
|
|
289
|
+
try {
|
|
290
|
+
execSync(`${pm.exec} drizzle-kit generate`, { cwd: projectDir, stdio: "pipe" });
|
|
291
|
+
s.stop("Migrations generated");
|
|
292
|
+
} catch {
|
|
293
|
+
s.stop("Migration generation failed");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
s.start("Applying migrations to remote D1");
|
|
297
|
+
try {
|
|
298
|
+
execSync(`${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`, {
|
|
299
|
+
cwd: projectDir,
|
|
300
|
+
stdio: "pipe",
|
|
301
|
+
});
|
|
302
|
+
cf.migrationsApplied = true;
|
|
303
|
+
s.stop("Migrations applied");
|
|
304
|
+
} catch {
|
|
305
|
+
s.stop("Migration apply failed — run manually with: wrangler d1 migrations apply --remote");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
219
312
|
// --- Done ---
|
|
220
313
|
|
|
221
314
|
if (target === "local") {
|
|
@@ -227,26 +320,25 @@ async function main() {
|
|
|
227
320
|
console.log(` To start again: cd ${projectName} && pnpm dev\n`);
|
|
228
321
|
}
|
|
229
322
|
} else {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"Set up Cloudflare resources:",
|
|
323
|
+
const lines = [`cd ${projectName}`];
|
|
324
|
+
const remaining = [];
|
|
325
|
+
if (!cf.d1Created) {
|
|
326
|
+
remaining.push(
|
|
235
327
|
` ${pm.exec} wrangler d1 create ${projectName}-db`,
|
|
236
328
|
" # Copy the database_id to wrangler.toml",
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (!cf.r2Created) {
|
|
332
|
+
remaining.push(` ${pm.exec} wrangler r2 bucket create ${projectName}-assets`);
|
|
333
|
+
}
|
|
334
|
+
if (!cf.migrationsApplied) {
|
|
335
|
+
remaining.push(` ${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`);
|
|
336
|
+
}
|
|
337
|
+
if (remaining.length > 0) {
|
|
338
|
+
lines.push("", "Remaining setup:", ...remaining);
|
|
339
|
+
}
|
|
340
|
+
lines.push("", "Local development:", ` ${pm.run} dev`, "", "Deploy:", " pnpm run deploy");
|
|
341
|
+
p.note(lines.join("\n"), "Next steps");
|
|
250
342
|
p.outro("Project created!");
|
|
251
343
|
}
|
|
252
344
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.12.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "astro dev",
|
|
11
|
+
"build": "astro build",
|
|
12
|
+
"preview": "astro preview",
|
|
13
|
+
"check": "astro check",
|
|
14
|
+
"cms:generate": "node --import tsx src/cms/internals/generator.ts",
|
|
15
|
+
"cms:admin": "node --import tsx src/cms/internals/create-admin.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@kidecms/core": "latest",
|
|
19
|
+
"@astrojs/node": "^10.0.1",
|
|
20
|
+
"@astrojs/react": "^5.0.0",
|
|
21
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
22
|
+
"@tailwindcss/vite": "^4.2.1",
|
|
23
|
+
"astro": "^6.0.4",
|
|
24
|
+
"better-sqlite3": "^12.6.2",
|
|
25
|
+
"drizzle-orm": "^0.45.1",
|
|
26
|
+
"react": "^19.2.4",
|
|
27
|
+
"react-dom": "^19.2.4",
|
|
28
|
+
"tailwindcss": "^4.2.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@astrojs/check": "^0.9.7",
|
|
32
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
33
|
+
"@types/react": "^19.2.14",
|
|
34
|
+
"@types/react-dom": "^19.2.3",
|
|
35
|
+
"drizzle-kit": "^0.31.9",
|
|
36
|
+
"tsx": "^4.21.0",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
5
|
+
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
6
|
+
|
|
7
|
+
let dbInstance: ReturnType<typeof drizzle> | null = null;
|
|
8
|
+
let sqliteInstance: InstanceType<typeof Database> | null = null;
|
|
9
|
+
let migrated = false;
|
|
10
|
+
|
|
11
|
+
const getDbPath = () => {
|
|
12
|
+
const url = process.env.CMS_DATABASE_URL;
|
|
13
|
+
if (url) return url;
|
|
14
|
+
return path.join(process.cwd(), "data", "cms.db");
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getDb = async () => {
|
|
18
|
+
if (dbInstance) return dbInstance;
|
|
19
|
+
|
|
20
|
+
const dbPath = getDbPath();
|
|
21
|
+
mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
22
|
+
|
|
23
|
+
sqliteInstance = new Database(dbPath);
|
|
24
|
+
sqliteInstance.pragma("journal_mode = WAL");
|
|
25
|
+
sqliteInstance.pragma("foreign_keys = ON");
|
|
26
|
+
|
|
27
|
+
dbInstance = drizzle(sqliteInstance);
|
|
28
|
+
|
|
29
|
+
if (!migrated) {
|
|
30
|
+
const migrationsFolder = path.join(process.cwd(), "src/cms/migrations");
|
|
31
|
+
try {
|
|
32
|
+
migrate(dbInstance, { migrationsFolder });
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore migration errors. In dev, tables may already exist via drizzle-kit push.
|
|
35
|
+
}
|
|
36
|
+
migrated = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return dbInstance;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const closeDb = () => {
|
|
43
|
+
if (sqliteInstance) {
|
|
44
|
+
sqliteInstance.close();
|
|
45
|
+
sqliteInstance = null;
|
|
46
|
+
dbInstance = null;
|
|
47
|
+
migrated = false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const sendInviteEmail = async (to: string, inviteUrl: string): Promise<boolean> => {
|
|
2
|
+
const apiKey = import.meta.env.RESEND_API_KEY;
|
|
3
|
+
if (!apiKey) return false;
|
|
4
|
+
|
|
5
|
+
const from = import.meta.env.RESEND_FROM_EMAIL ?? "Kide CMS <noreply@example.com>";
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${apiKey}`,
|
|
12
|
+
"Content-Type": "application/json",
|
|
13
|
+
},
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
from,
|
|
16
|
+
to,
|
|
17
|
+
subject: "You've been invited to Kide CMS",
|
|
18
|
+
html: `
|
|
19
|
+
<div style="font-family: sans-serif; max-width: 480px; margin: 0 auto;">
|
|
20
|
+
<h2 style="font-size: 20px;">You've been invited</h2>
|
|
21
|
+
<p>You've been invited to manage content on Kide CMS. Click the link below to set up your account:</p>
|
|
22
|
+
<p><a href="${inviteUrl}" style="display: inline-block; padding: 10px 20px; background: #0d9488; color: white; text-decoration: none; border-radius: 6px;">Accept invitation</a></p>
|
|
23
|
+
<p style="color: #666; font-size: 13px;">This link expires in 7 days. If you didn't expect this invitation, you can safely ignore it.</p>
|
|
24
|
+
</div>
|
|
25
|
+
`,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
return response.ok;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const isEmailConfigured = (): boolean => !!import.meta.env.RESEND_API_KEY;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
6
|
+
|
|
7
|
+
export async function putFile(storagePath: string, data: ArrayBuffer | Uint8Array): Promise<void> {
|
|
8
|
+
const filePath = path.join(publicDir, storagePath);
|
|
9
|
+
const dir = path.dirname(filePath);
|
|
10
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
11
|
+
const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
12
|
+
await writeFile(filePath, buffer);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function getFile(storagePath: string): Promise<ArrayBuffer | null> {
|
|
16
|
+
const filePath = path.join(publicDir, storagePath);
|
|
17
|
+
if (!existsSync(filePath)) return null;
|
|
18
|
+
const buffer = await readFile(filePath);
|
|
19
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function deleteFile(storagePath: string): Promise<void> {
|
|
23
|
+
const filePath = path.join(publicDir, storagePath);
|
|
24
|
+
try {
|
|
25
|
+
await unlink(filePath);
|
|
26
|
+
} catch {
|
|
27
|
+
// File may already be deleted.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineCollection, fields } from "@kidecms/core";
|
|
2
|
+
|
|
3
|
+
export default defineCollection({
|
|
4
|
+
slug: "users",
|
|
5
|
+
labels: { singular: "User", plural: "Users" },
|
|
6
|
+
auth: true,
|
|
7
|
+
timestamps: true,
|
|
8
|
+
views: {
|
|
9
|
+
list: { columns: ["name", "email", "role", "_updatedAt"] },
|
|
10
|
+
},
|
|
11
|
+
fields: {
|
|
12
|
+
name: fields.text({ required: true }),
|
|
13
|
+
email: fields.email({ required: true, unique: true }),
|
|
14
|
+
role: fields.select({
|
|
15
|
+
options: ["admin", "editor", "viewer"],
|
|
16
|
+
defaultValue: "editor",
|
|
17
|
+
}),
|
|
18
|
+
password: fields.text({ admin: { hidden: true } }),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { createAdminUser } from "@kidecms/core";
|
|
3
|
+
|
|
4
|
+
import "./runtime";
|
|
5
|
+
import { closeDb } from "../adapters/db";
|
|
6
|
+
|
|
7
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8
|
+
const ask = (question: string): Promise<string> =>
|
|
9
|
+
new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const name = await ask("Name: ");
|
|
13
|
+
if (!name) {
|
|
14
|
+
console.error("Name is required.");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const email = await ask("Email: ");
|
|
19
|
+
if (!email) {
|
|
20
|
+
console.error("Email is required.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const password = await ask("Password: ");
|
|
25
|
+
if (!password || password.length < 4) {
|
|
26
|
+
console.error("Password must be at least 4 characters.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
rl.close();
|
|
31
|
+
|
|
32
|
+
await createAdminUser({ name, email, password });
|
|
33
|
+
console.log(`Admin user "${name}" created.`);
|
|
34
|
+
closeDb();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch((error) => {
|
|
38
|
+
console.error(error.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { generate } from "@kidecms/core";
|
|
3
|
+
|
|
4
|
+
import config from "../cms.config";
|
|
5
|
+
|
|
6
|
+
await generate(config, {
|
|
7
|
+
outputDir: path.join(process.cwd(), "src/cms/.generated"),
|
|
8
|
+
runtimeImportPath: "../internals/runtime",
|
|
9
|
+
configImportPath: "../cms.config",
|
|
10
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
configureCmsRuntime,
|
|
3
|
+
initSchema,
|
|
4
|
+
createCms,
|
|
5
|
+
assets,
|
|
6
|
+
folders,
|
|
7
|
+
hashPassword,
|
|
8
|
+
verifyPassword,
|
|
9
|
+
createSession,
|
|
10
|
+
validateSession,
|
|
11
|
+
destroySession,
|
|
12
|
+
getSessionUser,
|
|
13
|
+
createInvite,
|
|
14
|
+
validateInvite,
|
|
15
|
+
consumeInvite,
|
|
16
|
+
SESSION_COOKIE_NAME,
|
|
17
|
+
setSessionCookie,
|
|
18
|
+
clearSessionCookie,
|
|
19
|
+
acquireLock,
|
|
20
|
+
releaseLock,
|
|
21
|
+
isAiEnabled,
|
|
22
|
+
getAiModel,
|
|
23
|
+
streamAltText,
|
|
24
|
+
streamSeoDescription,
|
|
25
|
+
streamTranslation,
|
|
26
|
+
} from "@kidecms/core";
|
|
27
|
+
|
|
28
|
+
import * as schema from "../.generated/schema";
|
|
29
|
+
import { closeDb, getDb } from "../adapters/db";
|
|
30
|
+
import { deleteFile, getFile, putFile } from "../adapters/storage";
|
|
31
|
+
import { isEmailConfigured, sendInviteEmail } from "../adapters/email";
|
|
32
|
+
|
|
33
|
+
let initialized = false;
|
|
34
|
+
|
|
35
|
+
export const initCmsRuntime = () => {
|
|
36
|
+
if (initialized) return;
|
|
37
|
+
|
|
38
|
+
initSchema(schema);
|
|
39
|
+
configureCmsRuntime({
|
|
40
|
+
getDb,
|
|
41
|
+
closeDb,
|
|
42
|
+
storage: { putFile, getFile, deleteFile },
|
|
43
|
+
email: { sendInviteEmail, isEmailConfigured },
|
|
44
|
+
env: (key) =>
|
|
45
|
+
(import.meta as ImportMeta & { env?: Record<string, string | undefined> }).env?.[key] ?? process.env[key],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
initialized = true;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
initCmsRuntime();
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
createCms,
|
|
55
|
+
assets,
|
|
56
|
+
folders,
|
|
57
|
+
hashPassword,
|
|
58
|
+
verifyPassword,
|
|
59
|
+
createSession,
|
|
60
|
+
validateSession,
|
|
61
|
+
destroySession,
|
|
62
|
+
getSessionUser,
|
|
63
|
+
createInvite,
|
|
64
|
+
validateInvite,
|
|
65
|
+
consumeInvite,
|
|
66
|
+
SESSION_COOKIE_NAME,
|
|
67
|
+
setSessionCookie,
|
|
68
|
+
clearSessionCookie,
|
|
69
|
+
acquireLock,
|
|
70
|
+
releaseLock,
|
|
71
|
+
isAiEnabled,
|
|
72
|
+
getAiModel,
|
|
73
|
+
streamAltText,
|
|
74
|
+
streamSeoDescription,
|
|
75
|
+
streamTranslation,
|
|
76
|
+
};
|