create-kide-app 0.0.1

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/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # create-kide-app
2
+
3
+ Scaffold a new [Kide CMS](https://github.com/mhernesniemi/kide-cms) project.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npm create kide-app my-project
9
+ # or
10
+ pnpm create kide-app my-project
11
+ # or
12
+ bunx create-kide-app my-project
13
+ ```
14
+
15
+ The CLI will guide you through:
16
+
17
+ 1. **Project name** — directory to create
18
+ 2. **Deploy target** — Local/Node.js or Cloudflare
19
+ 3. **Demo content** — optionally seed the database
20
+
21
+ ## What it does
22
+
23
+ - Downloads the latest Kide CMS template from GitHub
24
+ - Applies platform-specific configuration (Node.js adapter, Cloudflare D1/R2, etc.)
25
+ - Installs dependencies
26
+ - Generates the CMS schema
27
+ - Optionally seeds demo content
28
+
29
+ ## Requirements
30
+
31
+ - Node.js >= 22.12.0
package/index.js ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as p from "@clack/prompts";
4
+ import { execSync } from "node:child_process";
5
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const TEMPLATES_DIR = path.join(__dirname, "templates");
11
+
12
+ // --- Package manager detection ---
13
+
14
+ const pm = { name: "pnpm", exec: "pnpm dlx", run: "pnpm", install: "pnpm install" };
15
+
16
+ // --- Template repo URL ---
17
+ const REPO_URL = "https://github.com/mhernesniemi/kide-cms/archive/refs/heads/main.tar.gz";
18
+
19
+ // --- Main ---
20
+
21
+ async function main() {
22
+ p.intro("Create Kide CMS Project");
23
+
24
+ // 1. Project name
25
+ const projectName =
26
+ process.argv[2] ||
27
+ (await p.text({
28
+ message: "Project name",
29
+ placeholder: "my-cms-app",
30
+ validate: (value) => {
31
+ if (!value) return "Project name is required";
32
+ },
33
+ }));
34
+
35
+ if (p.isCancel(projectName)) {
36
+ p.cancel("Setup cancelled.");
37
+ process.exit(0);
38
+ }
39
+
40
+ const projectDir = path.resolve(process.cwd(), projectName);
41
+ if (existsSync(projectDir)) {
42
+ p.cancel(`Directory "${projectName}" already exists.`);
43
+ process.exit(1);
44
+ }
45
+
46
+ // 2. Deploy target
47
+ const target = await p.select({
48
+ message: "Where will you deploy?",
49
+ options: [
50
+ { label: "Local / Node.js", value: "local" },
51
+ { label: "Cloudflare", value: "cloudflare" },
52
+ ],
53
+ });
54
+
55
+ if (p.isCancel(target)) {
56
+ p.cancel("Setup cancelled.");
57
+ process.exit(0);
58
+ }
59
+
60
+ // 3. Demo content
61
+ const seedDemo = await p.confirm({
62
+ message: "Seed database with demo content?",
63
+ initialValue: false,
64
+ });
65
+
66
+ if (p.isCancel(seedDemo)) {
67
+ p.cancel("Setup cancelled.");
68
+ process.exit(0);
69
+ }
70
+
71
+ const s = p.spinner();
72
+
73
+ // --- Scaffold ---
74
+
75
+ s.start(`Scaffolding project (using ${pm.name})`);
76
+
77
+ mkdirSync(projectDir, { recursive: true });
78
+ const tmpArchive = path.join(projectDir, "_template.tar.gz");
79
+
80
+ try {
81
+ execSync(`curl -sL "${REPO_URL}" -o "${tmpArchive}"`, { stdio: "pipe" });
82
+ execSync(`tar -xzf "${tmpArchive}" -C "${projectDir}" --strip-components=1`, { stdio: "pipe" });
83
+ rmSync(tmpArchive, { force: true });
84
+ } catch {
85
+ s.message("Archive download failed, trying git clone...");
86
+ rmSync(projectDir, { recursive: true, force: true });
87
+ try {
88
+ execSync(`git clone --depth 1 https://github.com/mhernesniemi/kide-cms.git "${projectDir}"`, {
89
+ stdio: "pipe",
90
+ });
91
+ rmSync(path.join(projectDir, ".git"), { recursive: true, force: true });
92
+ } catch {
93
+ s.stop("Failed to download template.");
94
+ p.cancel("Check your network connection.");
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ // Remove files that shouldn't be in the scaffold
100
+ for (const remove of ["docs", "packages", "CLAUDE.md", ".claude", "data", ".cms-data", "dist", ".astro", ".env"]) {
101
+ const fp = path.join(projectDir, remove);
102
+ if (existsSync(fp)) rmSync(fp, { recursive: true, force: true });
103
+ }
104
+
105
+ s.stop("Project scaffolded");
106
+
107
+ // --- Apply target-specific files ---
108
+
109
+ s.start(`Applying ${target} configuration`);
110
+
111
+ const targetDir = path.join(TEMPLATES_DIR, target);
112
+
113
+ cpSync(path.join(targetDir, "astro.config.mjs"), path.join(projectDir, "astro.config.mjs"));
114
+ cpSync(path.join(targetDir, "db.ts"), path.join(projectDir, "src/cms/core/db.ts"));
115
+ cpSync(path.join(targetDir, "drizzle.config.ts"), path.join(projectDir, "drizzle.config.ts"));
116
+
117
+ const pkgPath = path.join(projectDir, "package.json");
118
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
119
+
120
+ pkg.name = projectName;
121
+
122
+ if (target === "cloudflare") {
123
+ delete pkg.dependencies["@astrojs/node"];
124
+ pkg.dependencies["@astrojs/cloudflare"] = "^12.0.0";
125
+
126
+ delete pkg.dependencies["better-sqlite3"];
127
+ if (pkg.devDependencies) delete pkg.devDependencies["@types/better-sqlite3"];
128
+ if (pkg.pnpm?.onlyBuiltDependencies) {
129
+ pkg.pnpm.onlyBuiltDependencies = pkg.pnpm.onlyBuiltDependencies.filter((d) => d !== "better-sqlite3");
130
+ if (pkg.pnpm.onlyBuiltDependencies.length === 0) delete pkg.pnpm.onlyBuiltDependencies;
131
+ if (Object.keys(pkg.pnpm).length === 0) delete pkg.pnpm;
132
+ }
133
+
134
+ let wranglerContent = readFileSync(path.join(targetDir, "wrangler.toml"), "utf-8");
135
+ wranglerContent = wranglerContent.replaceAll("{{PROJECT_NAME}}", projectName);
136
+ writeFileSync(path.join(projectDir, "wrangler.toml"), wranglerContent);
137
+
138
+ pkg.devDependencies.wrangler = "^4.0.0";
139
+
140
+ pkg.scripts.dev = "astro dev";
141
+ pkg.scripts.build = "astro build";
142
+ pkg.scripts.preview = "wrangler pages dev ./dist";
143
+ pkg.scripts.deploy = "astro build && wrangler pages deploy ./dist";
144
+ }
145
+
146
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
147
+
148
+ if (target === "cloudflare") {
149
+ const gitignorePath = path.join(projectDir, ".gitignore");
150
+ let gitignore = readFileSync(gitignorePath, "utf-8");
151
+ gitignore += "\n# Cloudflare\n.wrangler/\n";
152
+ writeFileSync(gitignorePath, gitignore);
153
+ }
154
+
155
+ s.stop("Configuration applied");
156
+
157
+ // --- Install dependencies ---
158
+
159
+ s.start("Installing dependencies");
160
+ try {
161
+ execSync(pm.install, { cwd: projectDir, stdio: "pipe" });
162
+ s.stop("Dependencies installed");
163
+ } catch {
164
+ s.stop(`${pm.install} failed — run it manually`);
165
+ }
166
+
167
+ // --- Generate schema ---
168
+
169
+ s.start("Generating CMS schema");
170
+ try {
171
+ execSync(`${pm.run} cms:generate`, { cwd: projectDir, stdio: "pipe" });
172
+ s.stop("Schema generated");
173
+ } catch {
174
+ s.stop("Schema generation failed — run `cms:generate` manually");
175
+ }
176
+
177
+ // --- Seed demo content ---
178
+
179
+ if (seedDemo && target === "local") {
180
+ s.start("Pushing schema to database");
181
+ try {
182
+ execSync(`${pm.exec} drizzle-kit push --force`, { cwd: projectDir, stdio: "pipe" });
183
+ s.stop("Schema pushed");
184
+ } catch {
185
+ s.stop("Schema push failed — will retry on dev start");
186
+ }
187
+ s.start("Seeding demo content");
188
+ try {
189
+ execSync(`${pm.run} cms:seed`, { cwd: projectDir, stdio: "pipe" });
190
+ s.stop("Demo content seeded");
191
+ } catch {
192
+ s.stop("Seeding failed — run `pnpm cms:seed` manually");
193
+ }
194
+ } else if (seedDemo && target === "cloudflare") {
195
+ p.note(
196
+ [
197
+ "Seeding for Cloudflare requires a D1 database.",
198
+ "",
199
+ ` ${pm.exec} wrangler d1 create ${projectName}-db`,
200
+ " # Add the database_id to wrangler.toml",
201
+ ` ${pm.exec} wrangler d1 execute --local --file=./src/cms/migrations/0000_*.sql`,
202
+ ` ${pm.run} cms:seed`,
203
+ ].join("\n"),
204
+ "Seed manually",
205
+ );
206
+ }
207
+
208
+ // --- Done ---
209
+
210
+ if (target === "local") {
211
+ p.outro("Starting dev server...");
212
+ try {
213
+ execSync(`${pm.run} dev`, { cwd: projectDir, stdio: "inherit" });
214
+ } catch {
215
+ console.log(`\n Project directory: ${projectDir}`);
216
+ console.log(` To start again: cd ${projectName} && pnpm dev\n`);
217
+ }
218
+ } else {
219
+ p.note(
220
+ [
221
+ "Set up Cloudflare resources:",
222
+ ` ${pm.exec} wrangler d1 create ${projectName}-db`,
223
+ ` ${pm.exec} wrangler r2 bucket create ${projectName}-assets`,
224
+ " # Add the database_id to wrangler.toml",
225
+ "",
226
+ "Local development:",
227
+ ` cd ${projectName} && ${pm.run} dev`,
228
+ "",
229
+ "Deploy:",
230
+ ` ${pm.run} deploy`,
231
+ ].join("\n"),
232
+ "Next steps",
233
+ );
234
+ p.outro("Project created!");
235
+ }
236
+ }
237
+
238
+ main().catch((err) => {
239
+ p.cancel(err.message);
240
+ process.exit(1);
241
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-kide-app",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a new Kide CMS project",
5
+ "author": "Matti Hernesniemi",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "create-kide-app": "./index.js"
10
+ },
11
+ "files": [
12
+ "index.js",
13
+ "templates"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/mhernesniemi/create-kide-app"
18
+ },
19
+ "homepage": "https://github.com/mhernesniemi/kide-cms",
20
+ "keywords": [
21
+ "astro",
22
+ "cms",
23
+ "kide",
24
+ "create-app",
25
+ "scaffold"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.12.0"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.1.0"
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+ import cloudflare from "@astrojs/cloudflare";
3
+ import react from "@astrojs/react";
4
+ import tailwindcss from "@tailwindcss/vite";
5
+ import { defineConfig } from "astro/config";
6
+ import cmsIntegration from "./src/cms/integration";
7
+
8
+ // https://astro.build/config
9
+ export default defineConfig({
10
+ output: "server",
11
+ integrations: [react(), cmsIntegration()],
12
+ adapter: cloudflare({
13
+ platformProxy: {
14
+ enabled: true,
15
+ },
16
+ }),
17
+ vite: {
18
+ plugins: [tailwindcss()],
19
+ resolve: {
20
+ // Ensure Cloudflare-compatible modules are used
21
+ conditions: ["workerd", "worker", "browser"],
22
+ },
23
+ },
24
+ });
@@ -0,0 +1,24 @@
1
+ import { drizzle } from "drizzle-orm/d1";
2
+
3
+ let dbInstance: ReturnType<typeof drizzle> | null = null;
4
+ let currentDb: D1Database | null = null;
5
+
6
+ export const getDb = async () => {
7
+ // Access the D1 binding from the Cloudflare runtime context
8
+ // The binding is set in wrangler.toml as "CMS_DB"
9
+ const env = (globalThis as any).__env__;
10
+ if (!env?.CMS_DB) {
11
+ throw new Error("D1 database binding CMS_DB not found. Check wrangler.toml.");
12
+ }
13
+
14
+ if (dbInstance && currentDb === env.CMS_DB) return dbInstance;
15
+
16
+ currentDb = env.CMS_DB;
17
+ dbInstance = drizzle(env.CMS_DB);
18
+ return dbInstance;
19
+ };
20
+
21
+ export const closeDb = () => {
22
+ dbInstance = null;
23
+ currentDb = null;
24
+ };
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/cms/.generated/schema.ts",
5
+ out: "./src/cms/migrations",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ // For local dev with wrangler, point to the local D1 database
9
+ url: ".wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite",
10
+ },
11
+ });
@@ -0,0 +1,13 @@
1
+ name = "{{PROJECT_NAME}}"
2
+ compatibility_date = "2025-01-01"
3
+ compatibility_flags = ["nodejs_compat"]
4
+
5
+ [[d1_databases]]
6
+ binding = "CMS_DB"
7
+ database_name = "{{PROJECT_NAME}}-db"
8
+ database_id = "" # Run: npx wrangler d1 create {{PROJECT_NAME}}-db
9
+
10
+ [[r2_buckets]]
11
+ binding = "CMS_ASSETS"
12
+ bucket_name = "{{PROJECT_NAME}}-assets"
13
+ # Run: npx wrangler r2 bucket create {{PROJECT_NAME}}-assets
@@ -0,0 +1,23 @@
1
+ // @ts-check
2
+ import node from "@astrojs/node";
3
+ import react from "@astrojs/react";
4
+ import tailwindcss from "@tailwindcss/vite";
5
+ import { defineConfig, memoryCache } from "astro/config";
6
+ import cmsIntegration from "./src/cms/integration";
7
+
8
+ // https://astro.build/config
9
+ export default defineConfig({
10
+ output: "server",
11
+ integrations: [react(), cmsIntegration()],
12
+ adapter: node({
13
+ mode: "standalone",
14
+ }),
15
+ vite: {
16
+ plugins: [tailwindcss()],
17
+ },
18
+ experimental: {
19
+ cache: {
20
+ provider: memoryCache(),
21
+ },
22
+ },
23
+ });
@@ -0,0 +1,50 @@
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
+ // Auto-run pending migrations on first connection (production only — dev uses drizzle-kit push)
30
+ if (!migrated) {
31
+ const migrationsFolder = path.join(process.cwd(), "src/cms/migrations");
32
+ try {
33
+ migrate(dbInstance, { migrationsFolder });
34
+ } catch {
35
+ // Ignore migration errors — tables may already exist via drizzle-kit push in dev
36
+ }
37
+ migrated = true;
38
+ }
39
+
40
+ return dbInstance;
41
+ };
42
+
43
+ export const closeDb = () => {
44
+ if (sqliteInstance) {
45
+ sqliteInstance.close();
46
+ sqliteInstance = null;
47
+ dbInstance = null;
48
+ migrated = false;
49
+ }
50
+ };
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/cms/.generated/schema.ts",
5
+ out: "./src/cms/migrations",
6
+ dialect: "sqlite",
7
+ dbCredentials: {
8
+ url: process.env.CMS_DATABASE_URL ?? "./data/cms.db",
9
+ },
10
+ });