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.
Files changed (41) hide show
  1. package/index.js +142 -50
  2. package/package.json +1 -1
  3. package/templates/base/.env.example +2 -0
  4. package/templates/base/package.json +39 -0
  5. package/templates/base/src/cms/adapters/db.ts +49 -0
  6. package/templates/base/src/cms/adapters/email.ts +34 -0
  7. package/templates/base/src/cms/adapters/storage.ts +29 -0
  8. package/templates/base/src/cms/cms.config.ts +7 -0
  9. package/templates/base/src/cms/collections/users.ts +20 -0
  10. package/templates/base/src/cms/internals/create-admin.ts +40 -0
  11. package/templates/base/src/cms/internals/generator.ts +10 -0
  12. package/templates/base/src/cms/internals/runtime.ts +76 -0
  13. package/templates/base/src/env.d.ts +12 -0
  14. package/templates/base/src/pages/index.astro +11 -0
  15. package/templates/base/src/styles/admin.css +253 -0
  16. package/templates/base/src/styles/public.css +2 -0
  17. package/templates/base/tsconfig.json +11 -0
  18. package/templates/cloudflare/astro.config.mjs +1 -1
  19. package/templates/cloudflare/uploads-route.ts +1 -1
  20. package/templates/demo/src/cms/cms.config.ts +17 -0
  21. package/templates/demo/src/cms/collections/authors.ts +20 -0
  22. package/templates/demo/src/cms/collections/front-page.ts +37 -0
  23. package/templates/demo/src/cms/collections/menus.ts +18 -0
  24. package/templates/demo/src/cms/collections/pages.ts +59 -0
  25. package/templates/demo/src/cms/collections/posts.ts +55 -0
  26. package/templates/demo/src/cms/collections/taxonomies.ts +18 -0
  27. package/templates/demo/src/cms/collections/users.ts +20 -0
  28. package/templates/demo/src/cms/internals/seed.data.ts +506 -0
  29. package/templates/demo/src/cms/internals/seed.ts +7 -0
  30. package/templates/demo/src/components/BlockRenderer.astro +32 -0
  31. package/templates/demo/src/components/RichTextContent.astro +18 -0
  32. package/templates/demo/src/components/blocks/Faq.astro +22 -0
  33. package/templates/demo/src/components/blocks/Hero.astro +16 -0
  34. package/templates/demo/src/components/blocks/Image.astro +25 -0
  35. package/templates/demo/src/components/blocks/Text.astro +16 -0
  36. package/templates/demo/src/layouts/PublicLayout.astro +54 -0
  37. package/templates/demo/src/pages/[...slug].astro +31 -0
  38. package/templates/demo/src/pages/blog/[slug].astro +41 -0
  39. package/templates/demo/src/pages/index.astro +51 -0
  40. package/templates/demo/src/scripts/preview.ts +33 -0
  41. 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, rmSync, writeFileSync } from "node:fs";
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
- mkdirSync(projectDir, { recursive: true });
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
- // Remove files that shouldn't be in the scaffold
104
- for (const remove of ["docs", "packages", "CLAUDE.md", ".claude", "data", ".cms-data", "dist", ".astro", ".env"]) {
105
- const fp = path.join(projectDir, remove);
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/core/db.ts"));
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/core/storage.ts"));
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
- p.note(
231
- [
232
- `cd ${projectName}`,
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
- ` ${pm.exec} wrangler r2 bucket create ${projectName}-assets`,
238
- "",
239
- "Push database schema:",
240
- ` ${pm.exec} wrangler d1 migrations apply ${projectName}-db --remote`,
241
- "",
242
- "Local development:",
243
- ` ${pm.run} dev`,
244
- "",
245
- "Deploy:",
246
- " pnpm run deploy",
247
- ].join("\n"),
248
- "Next steps",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-kide-app",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Scaffold a new Kide CMS project",
5
5
  "author": "Matti Hernesniemi",
6
6
  "license": "MIT",
@@ -0,0 +1,2 @@
1
+ # Optional: Enable AI features
2
+ # OPENAI_API_KEY=sk-...
@@ -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,7 @@
1
+ import { defineConfig } from "@kidecms/core";
2
+ import users from "./collections/users";
3
+
4
+ export default defineConfig({
5
+ database: { dialect: "sqlite" },
6
+ collections: [users],
7
+ });
@@ -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
+ };
@@ -0,0 +1,12 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ declare namespace App {
4
+ interface Locals {
5
+ user?: {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ role: string;
10
+ } | null;
11
+ }
12
+ }
@@ -0,0 +1,11 @@
1
+ <html lang="en">
2
+ <head>
3
+ <meta charset="utf-8" />
4
+ <meta name="viewport" content="width=device-width" />
5
+ <title>My Site</title>
6
+ </head>
7
+ <body>
8
+ <h1>Welcome</h1>
9
+ <p><a href="/admin">Open Admin</a></p>
10
+ </body>
11
+ </html>