create-kide-app 0.0.36 → 0.1.2

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 (50) hide show
  1. package/README.md +12 -11
  2. package/index.js +95 -420
  3. package/package.json +2 -3
  4. package/templates/base/.env.example +0 -2
  5. package/templates/base/CLAUDE.md +0 -73
  6. package/templates/base/gitignore +0 -8
  7. package/templates/base/package.json +0 -39
  8. package/templates/base/src/cms/adapters/db.ts +0 -49
  9. package/templates/base/src/cms/adapters/email.ts +0 -34
  10. package/templates/base/src/cms/adapters/storage.ts +0 -38
  11. package/templates/base/src/cms/cms.config.ts +0 -7
  12. package/templates/base/src/cms/collections/users.ts +0 -20
  13. package/templates/base/src/cms/internals/create-admin.ts +0 -40
  14. package/templates/base/src/cms/internals/generator.ts +0 -10
  15. package/templates/base/src/cms/internals/runtime.ts +0 -76
  16. package/templates/base/src/components/BlockRenderer.astro +0 -28
  17. package/templates/base/src/env.d.ts +0 -12
  18. package/templates/base/src/pages/index.astro +0 -11
  19. package/templates/base/src/styles/admin.css +0 -253
  20. package/templates/base/src/styles/public.css +0 -2
  21. package/templates/base/tsconfig.json +0 -11
  22. package/templates/cloudflare/astro.config.mjs +0 -16
  23. package/templates/cloudflare/db.ts +0 -23
  24. package/templates/cloudflare/drizzle.config.ts +0 -24
  25. package/templates/cloudflare/storage.ts +0 -25
  26. package/templates/cloudflare/uploads-route.ts +0 -36
  27. package/templates/cloudflare/wrangler.toml +0 -17
  28. package/templates/demo/src/cms/cms.config.ts +0 -17
  29. package/templates/demo/src/cms/collections/authors.ts +0 -20
  30. package/templates/demo/src/cms/collections/front-page.ts +0 -37
  31. package/templates/demo/src/cms/collections/menus.ts +0 -18
  32. package/templates/demo/src/cms/collections/pages.ts +0 -59
  33. package/templates/demo/src/cms/collections/posts.ts +0 -55
  34. package/templates/demo/src/cms/collections/taxonomies.ts +0 -18
  35. package/templates/demo/src/cms/collections/users.ts +0 -20
  36. package/templates/demo/src/cms/internals/seed.data.ts +0 -506
  37. package/templates/demo/src/cms/internals/seed.ts +0 -7
  38. package/templates/demo/src/components/BlockRenderer.astro +0 -32
  39. package/templates/demo/src/components/RichTextContent.astro +0 -18
  40. package/templates/demo/src/components/blocks/Faq.astro +0 -22
  41. package/templates/demo/src/components/blocks/Hero.astro +0 -19
  42. package/templates/demo/src/components/blocks/Image.astro +0 -25
  43. package/templates/demo/src/components/blocks/Text.astro +0 -16
  44. package/templates/demo/src/layouts/PublicLayout.astro +0 -53
  45. package/templates/demo/src/pages/[...slug].astro +0 -31
  46. package/templates/demo/src/pages/blog/[slug].astro +0 -41
  47. package/templates/demo/src/pages/index.astro +0 -51
  48. package/templates/local/astro.config.mjs +0 -23
  49. package/templates/local/db.ts +0 -48
  50. package/templates/local/drizzle.config.ts +0 -15
@@ -1,73 +0,0 @@
1
- # Kide CMS
2
-
3
- Code-first CMS built inside Astro 6.
4
-
5
- ## Structure
6
-
7
- ```
8
- src/cms/cms.config.ts # Collection definitions (source of truth)
9
- src/cms/collections/ # Individual collection files
10
- src/cms/adapters/ # Database, storage, email adapters
11
- src/cms/internals/ # Runtime, generator, seed scripts
12
- src/cms/.generated/ # Auto-generated — never edit
13
- src/components/ # Astro/React components
14
- src/layouts/ # Page layouts
15
- src/pages/ # Routes
16
- src/styles/public.css # Public site styles (plain Tailwind)
17
- ```
18
-
19
- ## Commands
20
-
21
- ```bash
22
- pnpm dev # start dev server (auto-generates schema + pushes DB)
23
- pnpm build # production build
24
- pnpm cms:generate # regenerate .generated/ from cms.config.ts
25
- pnpm cms:seed # seed database with demo content (if configured)
26
- ```
27
-
28
- ## Rules
29
-
30
- - Never edit files in `src/cms/.generated/` — they are overwritten on every generation.
31
- - Always query content through the typed local API: `cms.posts.find()`, `cms.pages.findOne()`, etc. Never bypass it with raw DB queries.
32
- - DB columns use `snake_case`, TypeScript fields use `camelCase`. System fields are prefixed with `_`.
33
- - Rich text is stored as JSON AST, never HTML or Markdown.
34
- - `labelField` on collections controls display name in the admin (fallback: title, name, or first text field).
35
-
36
- ## Stack
37
-
38
- Astro 6, React 19, Drizzle ORM, Zod, Tiptap, shadcn/ui, Tailwind CSS v4
39
-
40
- ## Field Types
41
-
42
- Defined in collection files via `fields.*`. All fields share base options: `label`, `description`, `required`, `defaultValue`, `indexed`, `unique`, `translatable`, `condition`, `admin`, `access`.
43
-
44
- | Field | Type-specific options |
45
- | ---------- | ------------------------------------------------------------------------------- |
46
- | `text` | `maxLength?: number` |
47
- | `slug` | `from?: string` — field to auto-generate slug from |
48
- | `email` | _(base only)_ |
49
- | `number` | _(base only)_ |
50
- | `boolean` | _(base only)_ |
51
- | `date` | _(base only)_ |
52
- | `select` | `options: string[]` **(required)** |
53
- | `richText` | _(base only)_ — stored as JSON AST `{ type: "root", children: RichTextNode[] }` |
54
- | `image` | _(base only)_ — stores asset reference |
55
- | `relation` | `collection: string` **(required)**, `hasMany?: boolean` |
56
- | `array` | `of: FieldConfig` **(required)** — field config for each item |
57
- | `json` | `schema?: string` |
58
- | `blocks` | `types: Record<string, Record<string, FieldConfig>>` **(required)** |
59
-
60
- `admin` sub-options: `component`, `placeholder`, `position` (`"content"` | `"sidebar"`), `rows`, `help`, `hidden`.
61
-
62
- `condition`: `{ field: string; value: string | string[] | boolean }` — show/hide field based on another field's value.
63
-
64
- `access`: `{ read?, update? }` — functions receiving `{ user, doc, operation, collection }`, return `boolean`.
65
-
66
- ## Live Preview
67
-
68
- The CMS auto-injects a preview script on every page. To make fields update in real time:
69
-
70
- 1. Add `?preview` to the page URL.
71
- 2. Add `data-cms="{fieldName}"` attributes to elements that display field values.
72
-
73
- For rich text and blocks fields, the preview system uses server-side rendering via `/api/cms/preview/render` (dev only). Simple text fields update via `textContent` directly.
@@ -1,8 +0,0 @@
1
- node_modules/
2
- dist/
3
- .astro/
4
- data/
5
- src/cms/.generated/
6
- public/uploads/
7
- .cms-cache/
8
- .env
@@ -1,39 +0,0 @@
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
- "@libsql/client": "^0.15.0",
22
- "@tailwindcss/typography": "^0.5.19",
23
- "@tailwindcss/vite": "^4.2.1",
24
- "astro": "^6.0.4",
25
- "drizzle-orm": "^0.45.1",
26
- "react": "^19.2.4",
27
- "react-dom": "^19.2.4",
28
- "tailwindcss": "^4.2.1",
29
- "tw-animate-css": "^1.4.0"
30
- },
31
- "devDependencies": {
32
- "@astrojs/check": "^0.9.7",
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
- }
@@ -1,49 +0,0 @@
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
- };
@@ -1,34 +0,0 @@
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;
@@ -1,38 +0,0 @@
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
- // Resolve storagePath safely under publicDir, rejecting any traversal attempts
8
- function safeResolve(storagePath: string): string {
9
- const resolved = path.resolve(publicDir, storagePath.replace(/^\/+/, ""));
10
- if (!resolved.startsWith(publicDir + path.sep) && resolved !== publicDir) {
11
- throw new Error(`Invalid storage path: ${storagePath}`);
12
- }
13
- return resolved;
14
- }
15
-
16
- export async function putFile(storagePath: string, data: ArrayBuffer | Uint8Array): Promise<void> {
17
- const filePath = safeResolve(storagePath);
18
- const dir = path.dirname(filePath);
19
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
20
- const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
21
- await writeFile(filePath, buffer);
22
- }
23
-
24
- export async function getFile(storagePath: string): Promise<ArrayBuffer | null> {
25
- const filePath = safeResolve(storagePath);
26
- if (!existsSync(filePath)) return null;
27
- const buffer = await readFile(filePath);
28
- return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
29
- }
30
-
31
- export async function deleteFile(storagePath: string): Promise<void> {
32
- const filePath = safeResolve(storagePath);
33
- try {
34
- await unlink(filePath);
35
- } catch {
36
- // File may already be deleted.
37
- }
38
- }
@@ -1,7 +0,0 @@
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
- });
@@ -1,20 +0,0 @@
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
- });
@@ -1,40 +0,0 @@
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
- });
@@ -1,10 +0,0 @@
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
- });
@@ -1,76 +0,0 @@
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
- };
@@ -1,28 +0,0 @@
1
- ---
2
- const { blocks = [] } = Astro.props as { blocks?: Array<Record<string, unknown>> };
3
-
4
- // Discover custom block components from src/components/blocks/
5
- const customComponents = import.meta.glob<{ default: any }>("./blocks/*.astro", { eager: true });
6
-
7
- const componentMap: Record<string, any> = {};
8
- for (const [path, mod] of Object.entries(customComponents)) {
9
- const filename = path.split("/").pop()?.replace(".astro", "") ?? "";
10
- const typeName = filename.charAt(0).toLowerCase() + filename.slice(1);
11
- componentMap[typeName] = mod.default;
12
- }
13
- ---
14
-
15
- <div>
16
- {
17
- blocks.map((block) => {
18
- const type = String(block.type ?? "");
19
- const Component = componentMap[type];
20
- if (!Component) {
21
- throw new Error(
22
- `No block component found for type "${type}". Add src/components/blocks/${type.charAt(0).toUpperCase() + type.slice(1)}.astro`,
23
- );
24
- }
25
- return <Component {...block} />;
26
- })
27
- }
28
- </div>
@@ -1,12 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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>